📋 Mục lục
- Giới thiệu về
useReducer - Khi nào nên sử dụng
useReducer? - Cách sử dụng
useReducer - Ví dụ thực tế: Danh sách công việc (Todo List)
- Thực hành tốt nhất với
useReducer - Các lỗi thường gặp và cách khắc phục
- Mẹo hiệu suất cho
useReducer - FAQ về
useReducer
1. Giới thiệu về useReducer
useReducer là một hook trong React, cung cấp một cách để quản lý trạng thái phức tạp hơn so với useState. Nếu bạn gặp phải một useState đang xử lý một đối tượng lớn với nhiều hàm cập nhật khác nhau, useReducer có thể là giải pháp tối ưu. Với useReducer, bạn có thể tập trung toàn bộ logic cập nhật trạng thái trong một hàm gọi là "reducer", giúp luồng dữ liệu trở nên dễ dự đoán hơn và dễ dàng gỡ lỗi.
2. Khi nào nên sử dụng useReducer?
Bạn nên cân nhắc sử dụng useReducer khi:
- Trạng thái tiếp theo phụ thuộc vào logic phức tạp dựa trên trạng thái trước đó.
- Trạng thái có cấu trúc phức tạp (đối tượng lồng nhau, mảng đối tượng).
- Logic cập nhật trạng thái cần được kiểm tra một cách riêng biệt.
- Bạn muốn tối ưu hóa hiệu suất cho các thành phần có các cập nhật sâu, vì
dispatchkhông thay đổi giữa các lần render.
3. Cách sử dụng useReducer
Cú pháp cơ bản để sử dụng useReducer như sau:
javascript
const [state, dispatch] = useReducer(reducer, initialState);
reducer: Một hàm thuần túy(state, action) => newState, nhận trạng thái hiện tại và một "hành động", và trả về trạng thái mới.initialState: Trạng thái khởi tạo, tương tự nhưuseState.state: Trạng thái hiện tại.dispatch: Một hàm "gửi" hành động đến hàmreducer. Đây là cách duy nhất để kích hoạt một cập nhật trạng thái. Một hành động thường là một đối tượng với một thuộc tínhtype(ví dụ:{ type: 'INCREMENT' }).
Luồng hoạt động: Thành phần → dispatch(action) → reducer(state, action) → Trạng thái mới → Render lại.
4. Ví dụ thực tế: Danh sách công việc (Todo List)
Đây là ví dụ điển hình cho useReducer, vì nó liên quan đến nhiều loại cập nhật trên một mảng đối tượng.
1. Định nghĩa Reducer và Trạng thái Khởi tạo
javascript
// components/TodoList.js
// Trạng thái khởi tạo: một mảng các công việc
const initialState = {
todos: [
{ id: 1, text: 'Học `useReducer`', completed: true },
{ id: 2, text: 'Thực hành với một ví dụ', completed: false },
],
};
// Các hành động có thể gửi đi
export const ACTIONS = {
ADD_TODO: 'add-todo',
TOGGLE_TODO: 'toggle-todo',
DELETE_TODO: 'delete-todo',
};
// Reducer chứa toàn bộ logic cập nhật
function reducer(state, action) {
switch (action.type) {
case ACTIONS.ADD_TODO:
return {
...state,
todos: [...state.todos, { id: Date.now(), text: action.payload.text, completed: false }],
};
case ACTIONS.TOGGLE_TODO:
return {
...state,
todos: state.todos.map(todo =>
todo.id === action.payload.id ? { ...todo, completed: !todo.completed } : todo
),
};
case ACTIONS.DELETE_TODO:
return {
...state,
todos: state.todos.filter(todo => todo.id !== action.payload.id),
};
default:
return state;
}
}
2. Sử dụng useReducer trong Thành phần
javascript
import React, { useReducer, useState } from 'react';
function TodoList() {
const [state, dispatch] = useReducer(reducer, initialState);
const [newTodoText, setNewTodoText] = useState('');
const handleSubmit = (e) => {
e.preventDefault();
if (newTodoText.trim() === '') return;
dispatch({ type: ACTIONS.ADD_TODO, payload: { text: newTodoText } });
setNewTodoText('');
};
return (
<div>
<h3>Danh sách công việc với `useReducer`</h3>
<form onSubmit={handleSubmit}>
<input
type="text"
value={newTodoText}
onChange={(e) => setNewTodoText(e.target.value)}
placeholder="Thêm công việc mới"
/>
<button type="submit">Thêm</button>
</form>
<ul>
{state.todos.map(todo => (
<li key={todo.id} style={{ textDecoration: todo.completed ? 'line-through' : 'none' }}>
{todo.text}
<button onClick={() => dispatch({ type: ACTIONS.TOGGLE_TODO, payload: { id: todo.id } })}>
{todo.completed ? 'Khôi phục' : 'Hoàn thành'}
</button>
<button onClick={() => dispatch({ type: ACTIONS.DELETE_TODO, payload: { id: todo.id } })}>
Xóa
</button>
</li>
))}
</ul>
</div>
);
}
5. Thực hành tốt nhất với useReducer
- Reducers Thuần túy: Một reducer không bao giờ nên sửa đổi trạng thái gốc (
state) hoặc hành động (action). Nó luôn phải trả về một đối tượng trạng thái mới và không nên thực hiện các tác dụng phụ (như gọi API). - Sử dụng
payloadcho dữ liệu: Thông thường, bạn nên truyền dữ liệu cần thiết cho việc cập nhật bên trong một thuộc tínhpayloadtrong đối tượng hành động. Ví dụ:{ type: 'ADD', payload: { newItem } }. - Xuất các hằng số hành động: Định nghĩa các
typecủa hành động dưới dạng hằng số (như trong ví dụACTIONS) giúp tránh lỗi chính tả và dễ bảo trì hơn. - Tách biệt reducer: Đối với các thành phần rất phức tạp hoặc để tái sử dụng logic, bạn có thể định nghĩa reducer và trạng thái khởi tạo trong một tệp riêng và nhập chúng vào.
6. Các lỗi thường gặp và cách khắc phục
-
Lỗi: Sửa đổi trạng thái trong reducer.
javascript// SAI ❌ case ACTIONS.DELETE_TODO: const index = state.todos.findIndex(t => t.id === action.payload.id); state.todos.splice(index, 1); return state;- Giải pháp: Luôn trả về một bản sao mới của trạng thái.
javascript// ĐÚNG ✅ case ACTIONS.DELETE_TODO: return { ...state, todos: state.todos.filter(t => t.id !== action.payload.id) }; -
Lỗi: Quên trường hợp
defaulttrongswitch.- Giải pháp: Nếu reducer của bạn nhận một hành động không nhận diện, nó nên trả về trạng thái hiện tại mà không thay đổi. Câu lệnh
default: return state;đảm nhận việc này. Nếu không có nó, reducer của bạn sẽ trả vềundefinedvà ứng dụng sẽ bị lỗi.
- Giải pháp: Nếu reducer của bạn nhận một hành động không nhận diện, nó nên trả về trạng thái hiện tại mà không thay đổi. Câu lệnh
7. Mẹo hiệu suất cho useReducer
- Tránh gọi các hàm không cần thiết: Đảm bảo chỉ gọi
dispatchkhi thực sự cần thiết để tránh render lại không cần thiết. - Sử dụng
React.memo: Nếu bạn có các thành phần con mà không cần cập nhật mỗi lần trạng thái thay đổi, hãy sử dụngReact.memođể cải thiện hiệu suất. - Tối ưu hóa reducer: Đảm bảo logic trong reducer được tối ưu hóa để không thực hiện các phép toán nặng nề không cần thiết trong mỗi lần gọi.
8. FAQ về useReducer
Q1: useReducer có khác gì so với useState?
A: useReducer thường được sử dụng khi cần quản lý trạng thái phức tạp hơn, trong khi useState phù hợp hơn cho các trạng thái đơn giản.
Q2: Tôi có thể sử dụng useReducer với các thành phần như thế nào?
A: Bạn có thể sử dụng useReducer trong bất kỳ thành phần nào của React, giống như useState.
Q3: Có thể sử dụng useReducer trong các custom hooks không?
A: Có, bạn hoàn toàn có thể sử dụng useReducer trong các custom hooks để quản lý trạng thái phức tạp và tái sử dụng logic.