Menu

Hook useReducer trong React: cú pháp, ví dụ, so với useState

useReducer lưu state và gom mọi cập nhật vào một hàm reducer: bạn dispatch một action, và reducer trả về state tiếp theo. Tìm hiểu cú pháp, cách viết reducer thuần, ví dụ danh sách việc cần làm, khởi tạo lười và khi nào nên chọn nó thay cho useState.

Trang này có trình soạn thảo chạy trực tiếp. Sửa, chạy và xem đầu ra ngay lập tức.

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
}
  • reducer là 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.
  • initialArg là state ban đầu, chỉ dùng ở lần render đầu tiên.
  • init không bắt buộc. Nếu bạn truyền nó, state ban đầu sẽ là init(initialArg).
  • state là 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.

useStateuseReducer
Logic cập nhậtTrong từng event handlerTrong một hàm reducer
Phù hợp choVài giá trị độc lậpNhiều field cùng bị nhiều sự kiện thay đổi
Lượng codeÍt hơn với state đơn giảnNhiều hơn lúc đầu, ít hơn khi số sự kiện tăng
Kiểm thửKiểm thử qua componentKiểm thử reducer như một hàm bình thường
Gỡ lỗiTì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.

Hình minh họa các ngôn ngữ lập trình của Coddy

Học lập trình cùng Coddy

Bắt đầu