useReducer là một hook của React giữ state giống useState, nhưng gom mọi cách thay đổi state vào một hàm duy nhất, gọi là reducer. Component gọi dispatch với một object action như { type: 'increment' }, và React truyền state hiện tại cùng action đó cho reducer, rồi reducer trả về state tiếp theo.
Các nút không nói con số thay đổi thế nào, chỉ nói điều gì đã xảy ra. Mọi phép tính đều nằm trong reducer. Thêm một case 'double' trả về { count: state.count * 2 } và một nút dispatch nó để xem một kiểu cập nhật mới khớp vào thế nào.
Cú pháp
const [state, dispatch] = useReducer(reducer, initialArg, init); // init is optional
function reducer(state, action) {
// return the next state
}
reducerlà một hàm(state, action) => nextState. Khai báo nó bên ngoài component; nó không cần gì từ phạm vi của component.initialArglà state ban đầu, chỉ dùng ở lần render đầu tiên.initkhông bắt buộc. Nếu bạn truyền nó, state ban đầu sẽ làinit(initialArg).statelà state hiện tại của lần render này.dispatch(action)gửi một action tới reducer và lên lịch render với kết quả. Nó ổn định: là cùng một hàm ở mọi lần render, nên có thể truyền xuống hoặc liệt kê làm dependency một cách an toàn.
Action có thể là giá trị bất kỳ, nhưng theo quy ước nó là một object có chuỗi type mô tả điều đã xảy ra, cùng với dữ liệu mà reducer cần: { type: 'added', text: 'Buy milk' }.
Viết một reducer
Hầu hết reducer là một câu lệnh switch theo action.type, mỗi loại sự kiện một case. Mỗi case trả về một state hoàn toàn mới; nó không bao giờ sửa state cũ. Case default ném lỗi, nên một lỗi gõ nhầm như dispatch({ type: 'incremnet' }) sẽ báo lỗi rõ ràng thay vì lặng lẽ không làm gì.
Đặt tên action theo việc người dùng đã làm (added, toggled, deleted), không theo thay đổi state mà bạn nghĩ tới (setTodos). Khi đó component đọc như một danh sách sự kiện, và reducer là nơi duy nhất quyết định mỗi sự kiện có nghĩa là gì.
Reducer phải thuần
Reducer là một hàm thuần: với cùng state và action, nó trả về cùng một kết quả, và không làm gì khác. Nghĩa là không thay đổi trực tiếp dữ liệu, không gửi request, không timer, không Math.random() hay Date.now() bên trong. React dựa vào điều này. Trong môi trường phát triển dưới StrictMode, React gọi reducer hai lần cho mỗi action và giữ một kết quả, để giúp bạn phát hiện reducer không thuần. Bản xem trước ở đây chạy như một bản build production, nên các ví dụ trên trang này chỉ gọi nó một lần.
Quy tắc không thay đổi trực tiếp là quy tắc hay bị vi phạm nhất. Push vào state.todos rồi trả về state nghĩa là trả về cùng một object, nên React không thấy thay đổi và bỏ qua lần render, đúng cái bẫy được mô tả ở trang cập nhật mảng và object. Hãy tạo mảng và object mới bằng spread, map và filter.
// Wrong: mutates and returns the same object
case 'added':
state.todos.push(action.todo);
return state;
// Right: returns a new object with a new array
case 'added':
return { ...state, todos: [...state.todos, action.todo] };
Side effect gắn với một sự kiện, như lưu lên máy chủ, đặt trong event handler cạnh dispatch. Side effect gắn với state, như đồng bộ nó vào localStorage, đặt trong một effect.
Danh sách việc cần làm với useReducer
Đây là reducer làm việc thật: thêm, đánh dấu và xóa việc cần làm. console.log ở đầu reducer in ra mọi action, một cách tiện lợi để theo dõi state thay đổi thế nào khi gỡ lỗi. Đây là side effect duy nhất thường được chấp nhận trong reducer, vì nó không thay đổi gì; hãy bỏ nó đi trước khi phát hành.
Thêm một việc, tích một ô và xóa một việc, rồi đọc console: mỗi thay đổi là một dòng ghi tên action và dữ liệu của nó. Chữ trong ô nhập vẫn nằm trong useState, vì nó chỉ thuộc về field đó và không sự kiện nào khác đụng tới. Dùng cả hai hook trong một component là chuyện bình thường.
Id của việc mới được tạo trong click handler và đi theo action, nên reducer chỉ việc sao chép nó. Viết id: nextId++ bên trong reducer sẽ làm nó không thuần: dưới StrictMode trong môi trường phát triển, lần gọi thứ hai sẽ bỏ qua mất một id.
dispatch không đổi state ngay lập tức
dispatch hoạt động giống setter của useState: nó lên lịch một lần render, và biến state trong handler hiện tại vẫn giữ giá trị cũ. Để dùng state mới ngay, hãy tự tính nó bằng cách gọi reducer.
Click vài lần. Dòng log đầu tiên luôn chậm một bước, còn dòng thứ hai khớp với con số trên nút sau khi render. Vì reducer là một hàm bình thường, tự gọi nó là an toàn, đó thêm một lợi ích của việc giữ nó thuần.
useState và useReducer
Cả hai hook đều lưu state, và mọi thứ viết được bằng hook này đều viết được bằng hook kia. Khác biệt nằm ở chỗ logic cập nhật được đặt ở đâu.
| useState | useReducer | |
|---|---|---|
| Logic cập nhật | Trong từng event handler | Trong một hàm reducer |
| Phù hợp cho | Vài giá trị độc lập | Nhiều field cùng bị nhiều sự kiện thay đổi |
| Lượng code | Ít hơn với state đơn giản | Nhiều hơn lúc đầu, ít hơn khi số sự kiện tăng |
| Kiểm thử | Kiểm thử qua component | Kiểm thử reducer như một hàm bình thường |
| Gỡ lỗi | Tìm handler nào đã set nó | Ghi log mọi action ở một chỗ |
Hãy dùng useReducer khi bạn thấy cùng một state bị cập nhật ở nhiều handler, khi một sự kiện phải thay đổi nhiều phần state cần luôn nhất quán, hoặc khi handler chủ yếu chứa logic tính state tiếp theo. Với một nút bật tắt hay một ô nhập chữ, useState ngắn gọn và rõ ràng hơn. Chuyển đổi về sau cũng không khó: thay setter bằng dispatch và chuyển logic vào các case.
Khởi tạo lười
Nếu bạn viết state ban đầu dưới dạng một lời gọi hàm, như useReducer(reducer, createInitialState('Ada')), lời gọi đó chạy ở mỗi lần render, dù React chỉ dùng kết quả của nó lần đầu tiên. Nếu việc dựng state ban đầu tốn kém, hoặc cần suy ra từ một prop, hãy truyền đối số thứ ba: một hàm init. React gọi init(initialArg) một lần, ở lần render đầu tiên.
Gõ vào ô nhập: component render ở mỗi lần gõ phím, nhưng createInitialState chỉ ghi log một lần. Giờ đổi lời gọi thành useReducer(reducer, createInitialState('Ada')) rồi gõ lại. Hàm chạy ở mỗi lần render, và từ lần thứ hai trở đi React đều bỏ kết quả đi.
Truyền chính hàm đó, không phải kết quả của việc gọi nó. useReducer(reducer, 'Ada', createInitialState) là khởi tạo lười; useReducer(reducer, createInitialState('Ada')) thì không.
useReducer với context
Reducer rất hợp với context khi một cây component sâu cần dùng state. Đặt state và dispatch vào context ở trên cùng, và mọi component bên dưới đều đọc được state hoặc gửi action mà không cần truyền props qua từng lớp. Vì dispatch không bao giờ đổi, các component chỉ gửi action có thể đọc một context dispatch riêng và không phải render lại khi state thay đổi.
import { createContext, useContext, useReducer } from 'react';
const TodosContext = createContext(null);
const TodosDispatchContext = createContext(null);
export function TodosProvider({ children }) {
const [todos, dispatch] = useReducer(todosReducer, []);
return (
<TodosContext value={todos}>
<TodosDispatchContext value={dispatch}>{children}</TodosDispatchContext>
</TodosContext>
);
}
function AddTodo() {
const dispatch = useContext(TodosDispatchContext);
return <button onClick={() => dispatch({ type: 'added', text: 'New' })}>Add</button>;
}
Trong React 19, một object context tự đóng vai provider của chính nó (<TodosContext value={...}>); <TodosContext.Provider> vẫn dùng được. Trang useContext giải thích cách context đến được các component và khi nào nó khiến chúng render lại.
Câu hỏi thường gặp
useReducer trong React là gì?
Là một hook cho state mà các cập nhật được mô tả dưới dạng action. Bạn gọi const [state, dispatch] = useReducer(reducer, initialState), rồi gọi dispatch({ type: 'added' }) từ các event handler. React truyền state hiện tại và action cho reducer của bạn, và giá trị nó trả về trở thành state tiếp theo.
Khi nào nên dùng useReducer thay vì useState?
Khi nhiều sự kiện cập nhật cùng một state theo những cách liên quan đến nhau, khi state tiếp theo phụ thuộc vào nhiều field cùng lúc, hoặc khi logic cập nhật đủ dài để bạn muốn đặt nó trong một hàm kiểm thử được, nằm ngoài component. Với một hai giá trị độc lập, useState đơn giản hơn.
Vì sao reducer phải là hàm thuần?
React có thể gọi reducer nhiều lần cho cùng một action (StrictMode cố tình làm vậy trong môi trường phát triển) và mong nhận cùng một kết quả mỗi lần. Vì thế reducer không được thay đổi trực tiếp state, lấy dữ liệu, đặt timer hay đọc giá trị ngẫu nhiên. Hãy làm những việc đó trong event handler hoặc effect.
dispatch có cập nhật state ngay lập tức không?
Không. Giống setter của useState, dispatch lên lịch một lần render. Bên trong event handler hiện tại, state vẫn giữ giá trị cũ; state mới xuất hiện ở lần render tiếp theo.
Đối số thứ ba của useReducer là gì?
Một hàm init không bắt buộc. Khi bạn truyền nó, React tính state ban đầu là init(initialArg), và chỉ ở lần render đầu tiên. Nó hữu ích khi việc dựng state ban đầu tốn kém hoặc phụ thuộc vào một prop.