useReducer là một React Hook cho phép bạn thêm một reducer vào component của bạn.

const [state, dispatch] = useReducer(reducer, initialArg, init?)

Tham khảo

useReducer(reducer, initialArg, init?)

Gọi useReducer ở cấp cao nhất của component để quản lý trạng thái của nó bằng một reducer.

import { useReducer } from 'react';

function reducer(state, action) {
// ...
}

function MyComponent() {
const [state, dispatch] = useReducer(reducer, { age: 42 });
// ...

Xem thêm các ví dụ bên dưới.

Tham số

  • reducer: Hàm reducer chỉ định cách trạng thái được cập nhật. Nó phải là thuần túy, nhận trạng thái và action làm đối số và trả về trạng thái tiếp theo. Trạng thái và action có thể thuộc bất kỳ loại nào.
  • initialArg: Giá trị từ đó trạng thái ban đầu được tính toán. Nó có thể là một giá trị của bất kỳ loại nào. Cách trạng thái ban đầu được tính toán từ nó phụ thuộc vào đối số init tiếp theo.
  • tùy chọn init: Hàm khởi tạo nên trả về trạng thái ban đầu. Nếu nó không được chỉ định, trạng thái ban đầu được đặt thành initialArg. Nếu không, trạng thái ban đầu được đặt thành kết quả của việc gọi init(initialArg).

Trả về

useReducer trả về một mảng với chính xác hai giá trị:

  1. Trạng thái hiện tại. Trong quá trình render đầu tiên, nó được đặt thành init(initialArg) hoặc initialArg (nếu không có init).
  2. Hàm dispatch cho phép bạn cập nhật trạng thái thành một giá trị khác và kích hoạt render lại.

Lưu ý

  • useReducer là một Hook, vì vậy bạn chỉ có thể gọi nó ở cấp cao nhất của component hoặc Hook của riêng bạn. Bạn không thể gọi nó bên trong các vòng lặp hoặc điều kiện. Nếu bạn cần điều đó, hãy trích xuất một component mới và di chuyển trạng thái vào đó.
  • Hàm dispatch có một định danh ổn định, vì vậy bạn sẽ thường thấy nó bị bỏ qua khỏi các dependencies của Effect, nhưng việc bao gồm nó sẽ không làm cho Effect kích hoạt. Nếu trình kiểm tra lỗi cho phép bạn bỏ qua một dependency mà không có lỗi, thì việc đó là an toàn. Tìm hiểu thêm về việc loại bỏ các dependencies của Effect.
  • Trong Strict Mode, React sẽ gọi reducer và trình khởi tạo của bạn hai lần để giúp bạn tìm thấy các tạp chất vô tình. Đây là hành vi chỉ dành cho phát triển và không ảnh hưởng đến sản xuất. Nếu reducer và trình khởi tạo của bạn là thuần túy (như chúng phải vậy), điều này sẽ không ảnh hưởng đến logic của bạn. Kết quả từ một trong các lệnh gọi bị bỏ qua.

Hàm dispatch

Hàm dispatch được trả về bởi useReducer cho phép bạn cập nhật trạng thái thành một giá trị khác và kích hoạt render lại. Bạn cần chuyển action làm đối số duy nhất cho hàm dispatch:

const [state, dispatch] = useReducer(reducer, { age: 42 });

function handleClick() {
dispatch({ type: 'incremented_age' });
// ...

React sẽ đặt trạng thái tiếp theo thành kết quả của việc gọi hàm reducer mà bạn đã cung cấp với state hiện tại và action bạn đã chuyển cho dispatch.

Tham số

  • action: Hành động được thực hiện bởi người dùng. Nó có thể là một giá trị của bất kỳ loại nào. Theo quy ước, một action thường là một đối tượng có thuộc tính type xác định nó và, tùy chọn, các thuộc tính khác với thông tin bổ sung.

Trả về

Hàm dispatch không có giá trị trả về.

Lưu ý

  • Hàm dispatch chỉ cập nhật biến trạng thái cho lần render tiếp theo. Nếu bạn đọc biến trạng thái sau khi gọi hàm dispatch, bạn vẫn sẽ nhận được giá trị cũ đã có trên màn hình trước khi bạn gọi.

  • Nếu giá trị mới bạn cung cấp giống hệt với state hiện tại, như được xác định bởi so sánh Object.is, React sẽ bỏ qua việc render lại component và các component con của nó. Đây là một tối ưu hóa. React vẫn có thể cần gọi component của bạn trước khi bỏ qua kết quả, nhưng nó không ảnh hưởng đến mã của bạn.

  • React gom các bản cập nhật trạng thái. Nó cập nhật màn hình sau khi tất cả các trình xử lý sự kiện đã chạy và đã gọi các hàm set của chúng. Điều này ngăn chặn nhiều lần render lại trong một sự kiện duy nhất. Trong trường hợp hiếm hoi bạn cần buộc React cập nhật màn hình sớm hơn, ví dụ: để truy cập DOM, bạn có thể sử dụng flushSync.


Cách sử dụng

Thêm một reducer vào một component

Gọi useReducer ở cấp cao nhất của component để quản lý trạng thái bằng một reducer.

import { useReducer } from 'react';

function reducer(state, action) {
// ...
}

function MyComponent() {
const [state, dispatch] = useReducer(reducer, { age: 42 });
// ...

useReducer trả về một mảng với chính xác hai mục:

  1. Trạng thái hiện tại của biến trạng thái này, ban đầu được đặt thành trạng thái ban đầu mà bạn đã cung cấp.
  2. Hàm dispatch cho phép bạn thay đổi nó để đáp ứng với tương tác.

Để cập nhật những gì trên màn hình, hãy gọi dispatch với một đối tượng đại diện cho những gì người dùng đã làm, được gọi là một action:

function handleClick() {
dispatch({ type: 'incremented_age' });
}

React sẽ chuyển trạng thái hiện tại và action cho hàm reducer của bạn. Reducer của bạn sẽ tính toán và trả về trạng thái tiếp theo. React sẽ lưu trữ trạng thái tiếp theo đó, render component của bạn với nó và cập nhật UI.

import { useReducer } from 'react';

function reducer(state, action) {
  if (action.type === 'incremented_age') {
    return {
      age: state.age + 1
    };
  }
  throw Error('Unknown action.');
}

export default function Counter() {
  const [state, dispatch] = useReducer(reducer, { age: 42 });

  return (
    <>
      <button onClick={() => {
        dispatch({ type: 'incremented_age' })
      }}>
        Increment age
      </button>
      <p>Hello! You are {state.age}.</p>
    </>
  );
}

useReducer rất giống với useState, nhưng nó cho phép bạn di chuyển logic cập nhật trạng thái từ các trình xử lý sự kiện vào một hàm duy nhất bên ngoài component của bạn. Đọc thêm về lựa chọn giữa useStateuseReducer.


Viết hàm reducer

Một hàm reducer được khai báo như thế này:

function reducer(state, action) {
// ...
}

Sau đó, bạn cần điền vào mã sẽ tính toán và trả về trạng thái tiếp theo. Theo quy ước, nó thường được viết dưới dạng một câu lệnh switch. Đối với mỗi case trong switch, hãy tính toán và trả về một số trạng thái tiếp theo.

function reducer(state, action) {
switch (action.type) {
case 'incremented_age': {
return {
name: state.name,
age: state.age + 1
};
}
case 'changed_name': {
return {
name: action.nextName,
age: state.age
};
}
}
throw Error('Unknown action: ' + action.type);
}

Actions có thể có bất kỳ hình dạng nào. Theo quy ước, người ta thường truyền các đối tượng có thuộc tính type xác định action. Nó nên bao gồm thông tin cần thiết tối thiểu mà reducer cần để tính toán trạng thái tiếp theo.

function Form() {
const [state, dispatch] = useReducer(reducer, { name: 'Taylor', age: 42 });

function handleButtonClick() {
dispatch({ type: 'incremented_age' });
}

function handleInputChange(e) {
dispatch({
type: 'changed_name',
nextName: e.target.value
});
}
// ...

Tên loại action là cục bộ đối với component của bạn. Mỗi action mô tả một tương tác duy nhất, ngay cả khi điều đó dẫn đến nhiều thay đổi trong dữ liệu. Hình dạng của trạng thái là tùy ý, nhưng thông thường nó sẽ là một đối tượng hoặc một mảng.

Đọc trích xuất logic trạng thái vào một reducer để tìm hiểu thêm.

Chú Ý

Trạng thái là chỉ đọc. Không sửa đổi bất kỳ đối tượng hoặc mảng nào trong trạng thái:

function reducer(state, action) {
switch (action.type) {
case 'incremented_age': {
// 🚩 Đừng đột biến một đối tượng trong trạng thái như thế này:
state.age = state.age + 1;
return state;
}

Thay vào đó, luôn trả về các đối tượng mới từ reducer của bạn:

function reducer(state, action) {
switch (action.type) {
case 'incremented_age': {
// ✅ Thay vào đó, hãy trả về một đối tượng mới
return {
...state,
age: state.age + 1
};
}

Đọc cập nhật các đối tượng trong statecập nhật các mảng trong state để tìm hiểu thêm.

Các ví dụ cơ bản về useReducer

Example 1 of 3:
Biểu mẫu (đối tượng)

Trong ví dụ này, reducer quản lý một đối tượng trạng thái với hai trường: nameage.

import { useReducer } from 'react';

function reducer(state, action) {
  switch (action.type) {
    case 'incremented_age': {
      return {
        name: state.name,
        age: state.age + 1
      };
    }
    case 'changed_name': {
      return {
        name: action.nextName,
        age: state.age
      };
    }
  }
  throw Error('Unknown action: ' + action.type);
}

const initialState = { name: 'Taylor', age: 42 };

export default function Form() {
  const [state, dispatch] = useReducer(reducer, initialState);

  function handleButtonClick() {
    dispatch({ type: 'incremented_age' });
  }

  function handleInputChange(e) {
    dispatch({
      type: 'changed_name',
      nextName: e.target.value
    }); 
  }

  return (
    <>
      <input
        value={state.name}
        onChange={handleInputChange}
      />
      <button onClick={handleButtonClick}>
        Increment age
      </button>
      <p>Hello, {state.name}. You are {state.age}.</p>
    </>
  );
}


Tránh tạo lại trạng thái ban đầu

React lưu trạng thái ban đầu một lần và bỏ qua nó trong các lần kết xuất tiếp theo.

function createInitialState(username) {
// ...
}

function TodoList({ username }) {
const [state, dispatch] = useReducer(reducer, createInitialState(username));
// ...
}

Mặc dù kết quả của createInitialState(username) chỉ được sử dụng cho lần kết xuất ban đầu, nhưng bạn vẫn gọi hàm này trên mỗi lần kết xuất. Điều này có thể gây lãng phí nếu nó tạo ra các mảng lớn hoặc thực hiện các tính toán tốn kém.

Để giải quyết vấn đề này, bạn có thể truyền nó như một hàm khởi tạo cho useReducer làm đối số thứ ba:

function createInitialState(username) {
// ...
}

function TodoList({ username }) {
const [state, dispatch] = useReducer(reducer, username, createInitialState);
// ...

Lưu ý rằng bạn đang truyền createInitialState, là chính hàm, chứ không phải createInitialState(), là kết quả của việc gọi nó. Bằng cách này, trạng thái ban đầu không bị tạo lại sau khi khởi tạo.

Trong ví dụ trên, createInitialState nhận một đối số username. Nếu trình khởi tạo của bạn không cần bất kỳ thông tin nào để tính toán trạng thái ban đầu, bạn có thể truyền null làm đối số thứ hai cho useReducer.

Sự khác biệt giữa việc truyền một trình khởi tạo và truyền trực tiếp trạng thái ban đầu

Example 1 of 2:
Truyền hàm khởi tạo

Ví dụ này truyền hàm khởi tạo, vì vậy hàm createInitialState chỉ chạy trong quá trình khởi tạo. Nó không chạy khi thành phần kết xuất lại, chẳng hạn như khi bạn nhập vào đầu vào.

import { useReducer } from 'react';

function createInitialState(username) {
  const initialTodos = [];
  for (let i = 0; i < 50; i++) {
    initialTodos.push({
      id: i,
      text: username + "'s task #" + (i + 1)
    });
  }
  return {
    draft: '',
    todos: initialTodos,
  };
}

function reducer(state, action) {
  switch (action.type) {
    case 'changed_draft': {
      return {
        draft: action.nextDraft,
        todos: state.todos,
      };
    };
    case 'added_todo': {
      return {
        draft: '',
        todos: [{
          id: state.todos.length,
          text: state.draft
        }, ...state.todos]
      }
    }
  }
  throw Error('Unknown action: ' + action.type);
}

export default function TodoList({ username }) {
  const [state, dispatch] = useReducer(
    reducer,
    username,
    createInitialState
  );
  return (
    <>
      <input
        value={state.draft}
        onChange={e => {
          dispatch({
            type: 'changed_draft',
            nextDraft: e.target.value
          })
        }}
      />
      <button onClick={() => {
        dispatch({ type: 'added_todo' });
      }}>Add</button>
      <ul>
        {state.todos.map(item => (
          <li key={item.id}>
            {item.text}
          </li>
        ))}
      </ul>
    </>
  );
}


Khắc phục sự cố

Tôi đã gửi một action, nhưng nhật ký cho tôi giá trị trạng thái cũ

Gọi hàm dispatch không thay đổi trạng thái trong mã đang chạy:

function handleClick() {
console.log(state.age); // 42

dispatch({ type: 'incremented_age' }); // Yêu cầu kết xuất lại với 43
console.log(state.age); // Vẫn là 42!

setTimeout(() => {
console.log(state.age); // Cũng là 42!
}, 5000);
}

Điều này là do trạng thái hoạt động như một ảnh chụp nhanh. Cập nhật trạng thái yêu cầu một kết xuất khác với giá trị trạng thái mới, nhưng không ảnh hưởng đến biến JavaScript state trong trình xử lý sự kiện đang chạy của bạn.

Nếu bạn cần đoán giá trị trạng thái tiếp theo, bạn có thể tính toán nó theo cách thủ công bằng cách tự gọi reducer:

const action = { type: 'incremented_age' };
dispatch(action);

const nextState = reducer(state, action);
console.log(state); // { age: 42 }
console.log(nextState); // { age: 43 }

Tôi đã gửi một action, nhưng màn hình không cập nhật

React sẽ bỏ qua bản cập nhật của bạn nếu trạng thái tiếp theo bằng với trạng thái trước đó, như được xác định bởi so sánh Object.is. Điều này thường xảy ra khi bạn thay đổi trực tiếp một đối tượng hoặc một mảng trong trạng thái:

function reducer(state, action) {
switch (action.type) {
case 'incremented_age': {
// 🚩 Sai: đột biến đối tượng hiện có
state.age++;
return state;
}
case 'changed_name': {
// 🚩 Sai: đột biến đối tượng hiện có
state.name = action.nextName;
return state;
}
// ...
}
}

Bạn đã đột biến một đối tượng state hiện có và trả về nó, vì vậy React đã bỏ qua bản cập nhật. Để khắc phục điều này, bạn cần đảm bảo rằng bạn luôn cập nhật các đối tượng trong trạng tháicập nhật các mảng trong trạng thái thay vì đột biến chúng:

function reducer(state, action) {
switch (action.type) {
case 'incremented_age': {
// ✅ Đúng: tạo một đối tượng mới
return {
...state,
age: state.age + 1
};
}
case 'changed_name': {
// ✅ Đúng: tạo một đối tượng mới
return {
...state,
name: action.nextName
};
}
// ...
}
}

Một phần trạng thái reducer của tôi trở thành không xác định sau khi gửi

Đảm bảo rằng mọi nhánh case sao chép tất cả các trường hiện có khi trả về trạng thái mới:

function reducer(state, action) {
switch (action.type) {
case 'incremented_age': {
return {
...state, // Đừng quên điều này!
age: state.age + 1
};
}
// ...

Nếu không có ...state ở trên, trạng thái tiếp theo được trả về sẽ chỉ chứa trường age và không có gì khác.


Toàn bộ trạng thái reducer của tôi trở thành không xác định sau khi gửi

Nếu trạng thái của bạn bất ngờ trở thành undefined, có thể bạn đang quên return trạng thái trong một trong các trường hợp hoặc loại action của bạn không khớp với bất kỳ câu lệnh case nào. Để tìm lý do, hãy đưa ra một lỗi bên ngoài switch:

function reducer(state, action) {
switch (action.type) {
case 'incremented_age': {
// ...
}
case 'edited_name': {
// ...
}
}
throw Error('Unknown action: ' + action.type);
}

Bạn cũng có thể sử dụng trình kiểm tra kiểu tĩnh như TypeScript để bắt các lỗi như vậy.


Tôi gặp lỗi: “Quá nhiều lần kết xuất lại”

Bạn có thể gặp lỗi cho biết: Too many re-renders. React limits the number of renders to prevent an infinite loop. (Quá nhiều lần kết xuất lại. React giới hạn số lần kết xuất để ngăn chặn vòng lặp vô hạn.) Thông thường, điều này có nghĩa là bạn đang gửi một action vô điều kiện trong quá trình kết xuất, vì vậy thành phần của bạn đi vào một vòng lặp: kết xuất, gửi (gây ra kết xuất), kết xuất, gửi (gây ra kết xuất), v.v. Rất thường xuyên, điều này là do một sai lầm trong việc chỉ định một trình xử lý sự kiện:

// 🚩 Sai: gọi trình xử lý trong quá trình kết xuất
return <button onClick={handleClick()}>Click me</button>

// ✅ Đúng: chuyển trình xử lý sự kiện xuống
return <button onClick={handleClick}>Click me</button>

// ✅ Đúng: chuyển một hàm nội tuyến xuống
return <button onClick={(e) => handleClick(e)}>Click me</button>

Nếu bạn không thể tìm thấy nguyên nhân của lỗi này, hãy nhấp vào mũi tên bên cạnh lỗi trong bảng điều khiển và xem qua ngăn xếp JavaScript để tìm lệnh gọi hàm dispatch cụ thể chịu trách nhiệm cho lỗi.


Hàm reducer hoặc hàm khởi tạo của tôi chạy hai lần

Trong Chế độ nghiêm ngặt, React sẽ gọi các hàm reducer và hàm khởi tạo của bạn hai lần. Điều này sẽ không phá vỡ mã của bạn.

Hành vi chỉ dành cho phát triển này giúp bạn giữ cho các thành phần thuần túy. React sử dụng kết quả của một trong các lệnh gọi và bỏ qua kết quả của lệnh gọi kia. Miễn là thành phần, trình khởi tạo và các hàm reducer của bạn là thuần túy, điều này sẽ không ảnh hưởng đến logic của bạn. Tuy nhiên, nếu chúng vô tình không thuần túy, điều này sẽ giúp bạn nhận thấy những sai lầm.

Ví dụ: hàm reducer không thuần túy này đột biến một mảng trong trạng thái:

function reducer(state, action) {
switch (action.type) {
case 'added_todo': {
// 🚩 Sai lầm: đột biến trạng thái
state.todos.push({ id: nextId++, text: action.text });
return state;
}
// ...
}
}

Vì React gọi hàm reducer của bạn hai lần, bạn sẽ thấy todo đã được thêm hai lần, vì vậy bạn sẽ biết rằng có một sai lầm. Trong ví dụ này, bạn có thể sửa sai lầm bằng cách thay thế mảng thay vì đột biến nó:

function reducer(state, action) {
switch (action.type) {
case 'added_todo': {
// ✅ Đúng: thay thế bằng trạng thái mới
return {
...state,
todos: [
...state.todos,
{ id: nextId++, text: action.text }
]
};
}
// ...
}
}

Bây giờ hàm reducer này là thuần túy, việc gọi nó thêm một lần không tạo ra sự khác biệt trong hành vi. Đây là lý do tại sao React gọi nó hai lần giúp bạn tìm thấy những sai lầm. Chỉ các hàm thành phần, trình khởi tạo và reducer cần phải thuần túy. Các trình xử lý sự kiện không cần phải thuần túy, vì vậy React sẽ không bao giờ gọi các trình xử lý sự kiện của bạn hai lần.

Đọc giữ cho các thành phần thuần túy để tìm hiểu thêm.