useReducer 是一个 React Hook,它允许你向你的组件添加一个 reducer

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

参考

🌐 Reference

useReducer(reducer, initialArg, init?)

在组件的顶层调用 useReducer 来使用 reducer 管理其状态

🌐 Call useReducer at the top level of your component to manage its state with a reducer.

import { useReducer } from 'react';

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

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

查看更多示例。

参数

🌐 Parameters

  • reducer:指定状态如何更新的 reducer 函数。它必须是纯函数,应以状态和动作作为参数,并应返回下一个状态。状态和动作可以是任何类型。
  • initialArg:用于计算初始状态的值。它可以是任何类型的值。初始状态如何从它计算取决于下一个 init 参数。
  • 可选 init:初始化函数,应返回初始状态。如果未指定,初始状态将设置为 initialArg。否则,初始状态将设置为调用 init(initialArg) 的结果。

返回

🌐 Returns

useReducer 返回一个恰好包含两个值的数组:

  1. 当前状态。在第一次渲染时,它被设置为 init(initialArg)initialArg(如果没有 init)。
  2. dispatch 函数 允许你将状态更新为不同的值并触发重新渲染。

注意事项

🌐 Caveats

  • useReducer 是一个 Hook,因此你只能在组件的顶层或你自己的 Hook 中调用它。你不能在循环或条件语句中调用它。如果你需要那样做,提取一个新的组件并将状态移动到其中。
  • dispatch 函数具有稳定的标识,因此你经常会看到它在 Effect 依赖中被省略,但包括它不会导致 Effect 被触发。如果 linter 允许你在没有错误的情况下省略依赖,这是安全的。 了解更多关于移除 Effect 依赖的信息。
  • 在严格模式下,React 会调用你的 reducer 和 initializer 两次,以便帮助你发现意外的副作用。这是仅在开发环境中的行为,对生产环境没有影响。如果你的 reducer 和 initializer 是纯函数(应该如此),这不会影响你的逻辑。其中一次调用的结果会被忽略。

dispatch 函数

🌐 dispatch function

useReducer 返回的 dispatch 函数让你可以将状态更新为不同的值并触发重新渲染。你需要将操作作为唯一参数传递给 dispatch 函数:

🌐 The dispatch function returned by useReducer lets you update the state to a different value and trigger a re-render. You need to pass the action as the only argument to the dispatch function:

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

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

React 将把下一个状态设置为调用你提供的 reducer 函数的结果,该函数使用当前的 state 和你传递给 dispatch 的动作。

🌐 React will set the next state to the result of calling the reducer function you’ve provided with the current state and the action you’ve passed to dispatch.

参数

🌐 Parameters

  • action:用户执行的操作。它可以是任何类型的值。按惯例,操作通常是一个具有 type 属性的对象,用于标识它,并且可选地包含其他属性以提供附加信息。

返回

🌐 Returns

dispatch 函数没有返回值。

注意事项

🌐 Caveats

  • dispatch 函数仅会更新 下一次 渲染的状态变量。如果在调用 dispatch 函数后读取状态变量,你仍然会得到调用前屏幕上显示的旧值
  • 如果你提供的新值与当前的 state 相同(通过 Object.is 比较确定),React 将**跳过重新渲染组件及其子组件。**这是一个优化。React 可能仍然需要在忽略结果之前调用你的组件,但这不应该影响你的代码。
  • React 批量处理状态更新。 它会在所有事件处理程序运行完毕并调用它们的 set 函数后更新屏幕。这可以防止在单个事件中发生多次重新渲染。在极少数情况下,如果你需要强制 React 提前更新屏幕,例如访问 DOM,你可以使用 flushSync.

用法

🌐 Usage

向组件添加 reducer

🌐 Adding a reducer to a component

在组件的顶层调用 useReducer 来使用 reducer 管理状态。

🌐 Call useReducer at the top level of your component to manage state with a reducer.

import { useReducer } from 'react';

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

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

useReducer 返回一个恰好包含两个元素的数组:

  1. 这个状态变量的 当前状态 ,最初设置为你提供的 初始状态
  2. The dispatch 函数 允许你根据交互进行更改。

要更新屏幕上的内容,请调用 dispatch ,并传入一个表示用户操作的对象,称为action

🌐 To update what’s on the screen, call dispatch with an object representing what the user did, called an action:

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

React 会将当前状态和操作传递给你的 reducer 函数。你的 reducer 会计算并返回下一个状态。React 会存储该下一个状态,用它渲染你的组件,并更新用户界面。

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>
    </>
  );
}

useReduceruseState 非常相似,但它允许你将状态更新逻辑从事件处理程序移动到组件外的单个函数中。阅读更多关于 useStateuseReducer 之间进行选择 的内容。


编写 reducer 函数

🌐 Writing the reducer function

reducer 函数声明如下:

🌐 A reducer function is declared like this:

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

然后你需要填写将计算并返回下一状态的代码。按照惯例,通常将其写为 switch 语句。 对于 switch 中的每个 case,计算并返回某个下一状态。

🌐 Then you need to fill in the code that will calculate and return the next state. By convention, it is common to write it as a switch statement. For each case in the switch, calculate and return some next state.

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);
}

动作可以有任何形式。按照惯例,通常会传递具有 type 属性的对象来标识动作。它应包含 reducer 计算下一个状态所需的最少信息。

🌐 Actions can have any shape. By convention, it’s common to pass objects with a type property identifying the action. It should include the minimal necessary information that the reducer needs to compute the next state.

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
});
}
// ...

操作类型名称是本地于你的组件的。每个操作描述一个单一的交互,即使这会导致数据的多次更改。 状态的形状是任意的,但通常它会是一个对象或数组。

🌐 The action type names are local to your component. Each action describes a single interaction, even if that leads to multiple changes in data. The shape of the state is arbitrary, but usually it’ll be an object or an array.

阅读 将状态逻辑提取到 reducer 中 以了解更多信息。

🌐 Read extracting state logic into a reducer to learn more.

易犯错误

状态是只读的。不要修改状态中的任何对象或数组:

🌐 State is read-only. Don’t modify any objects or arrays in state:

function reducer(state, action) {
switch (action.type) {
case 'incremented_age': {
// 🚩 Don't mutate an object in state like this:
state.age = state.age + 1;
return state;
}

而是,总是从你的 reducer 返回新对象:

🌐 Instead, always return new objects from your reducer:

function reducer(state, action) {
switch (action.type) {
case 'incremented_age': {
// ✅ Instead, return a new object
return {
...state,
age: state.age + 1
};
}

阅读在状态中更新对象在状态中更新数组以了解更多信息。

🌐 Read updating objects in state and updating arrays in state to learn more.

Basic useReducer examples

例子 1 of 3:
表单(对象)

🌐 Form (object)

在这个例子中,reducer 管理一个具有两个字段的状态对象:nameage

🌐 In this example, the reducer manages a state object with two fields: name and age.

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>
    </>
  );
}


避免重新创建初始状态

🌐 Avoiding recreating the initial state

React 会保存一次初始状态,并在下一次渲染时忽略它。

🌐 React saves the initial state once and ignores it on the next renders.

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

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

尽管 createInitialState(username) 的结果仅用于初始渲染,但你仍然在每次渲染时调用这个函数。如果它在创建大型数组或执行昂贵的计算时,这可能会很浪费。

🌐 Although the result of createInitialState(username) is only used for the initial render, you’re still calling this function on every render. This can be wasteful if it’s creating large arrays or performing expensive calculations.

为了解决这个问题,你可以将其作为 初始化器 函数传递给 useReducer,作为第三个参数,如下所示:

🌐 To solve this, you may pass it as an initializer function to useReducer as the third argument instead:

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

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

注意你传递的是 createInitialState,它是函数本身,而不是 createInitialState(),它是调用函数的结果。这样,初始状态在初始化后不会被重新创建。

🌐 Notice that you’re passing createInitialState, which is the function itself, and not createInitialState(), which is the result of calling it. This way, the initial state does not get re-created after initialization.

在上述例子中,createInitialState 接受一个 username 参数。如果你的初始化器不需要任何信息来计算初始状态,你可以将 null 作为第二个参数传递给 useReducer

🌐 In the above example, createInitialState takes a username argument. If your initializer doesn’t need any information to compute the initial state, you may pass null as the second argument to useReducer.

The difference between passing an initializer and passing the initial state directly

例子 1 of 2:
传递初始化函数

🌐 Passing the initializer function

这个示例传递了初始化函数,因此 createInitialState 函数只在初始化时运行。它在组件重新渲染时不会运行,例如当你在输入框中输入内容时。

🌐 This example passes the initializer function, so the createInitialState function only runs during initialization. It does not run when component re-renders, such as when you type into the input.

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>
    </>
  );
}


故障排除

🌐 Troubleshooting

我已经发送了一个动作,但是日志记录给了我旧的状态值

🌐 I’ve dispatched an action, but logging gives me the old state value

调用 dispatch 函数不会改变运行代码中的状态

🌐 Calling the dispatch function does not change state in the running code:

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

dispatch({ type: 'incremented_age' }); // Request a re-render with 43
console.log(state.age); // Still 42!

setTimeout(() => {
console.log(state.age); // Also 42!
}, 5000);
}

这是因为状态表现得像一个快照。 更新状态会请求使用新的状态值重新渲染,但不会影响你在已运行事件处理程序中的 state JavaScript 变量。

🌐 This is because states behaves like a snapshot. Updating state requests another render with the new state value, but does not affect the state JavaScript variable in your already-running event handler.

如果需要猜测下一个状态值,可以自己调用 reducer 手动计算:

🌐 If you need to guess the next state value, you can calculate it manually by calling the reducer yourself:

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

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

我已经发送了一个动作,但屏幕没有更新

🌐 I’ve dispatched an action, but the screen doesn’t update

如果下一个状态与先前状态相等,React 会**忽略你的更新,**这一点是通过 Object.is 比较确定的。这通常发生在你直接更改状态中的对象或数组时:

🌐 React will ignore your update if the next state is equal to the previous state, as determined by an Object.is comparison. This usually happens when you change an object or an array in state directly:

function reducer(state, action) {
switch (action.type) {
case 'incremented_age': {
// 🚩 Wrong: mutating existing object
state.age++;
return state;
}
case 'changed_name': {
// 🚩 Wrong: mutating existing object
state.name = action.nextName;
return state;
}
// ...
}
}

你修改了一个现有的 state 对象并返回它,所以 React 忽略了更新。要解决此问题,你需要确保总是更新状态中的对象更新状态中的数组,而不是直接修改它们:

🌐 You mutated an existing state object and returned it, so React ignored the update. To fix this, you need to ensure that you’re always updating objects in state and updating arrays in state instead of mutating them:

function reducer(state, action) {
switch (action.type) {
case 'incremented_age': {
// ✅ Correct: creating a new object
return {
...state,
age: state.age + 1
};
}
case 'changed_name': {
// ✅ Correct: creating a new object
return {
...state,
name: action.nextName
};
}
// ...
}
}

调度后我的 reducer 状态的一部分变得未定义

🌐 A part of my reducer state becomes undefined after dispatching

确保每个 case 分支在返回新状态时复制所有现有字段

🌐 Make sure that every case branch copies all of the existing fields when returning the new state:

function reducer(state, action) {
switch (action.type) {
case 'incremented_age': {
return {
...state, // Don't forget this!
age: state.age + 1
};
}
// ...

在没有上方的 ...state 的情况下,返回的下一个状态将只包含 age 字段,而不包含其他内容。

🌐 Without ...state above, the returned next state would only contain the age field and nothing else.


调度后我的整个 reducer 状态变得未定义

🌐 My entire reducer state becomes undefined after dispatching

如果你的状态意外地变成 undefined,你很可能在某个情况下忘记了 return 状态,或者你的动作类型与任何 case 语句都不匹配。要找出原因,请在 switch 外抛出一个错误:

🌐 If your state unexpectedly becomes undefined, you’re likely forgetting to return state in one of the cases, or your action type doesn’t match any of the case statements. To find why, throw an error outside the switch:

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

你还可以使用 TypeScript 等静态类型检查器来捕获此类错误。

🌐 You can also use a static type checker like TypeScript to catch such mistakes.


我收到一个错误:“重新渲染次数过多”

🌐 I’m getting an error: “Too many re-renders”

你可能会收到一个错误提示:Too many re-renders. React limits the number of renders to prevent an infinite loop.。通常,这意味着你在渲染期间无条件地分发了一个动作,因此你的组件进入了一个循环:渲染,分发(导致渲染),渲染,分发(导致渲染),以此类推。通常,这种情况是由在指定事件处理程序时的错误引起的:

🌐 You might get an error that says: Too many re-renders. React limits the number of renders to prevent an infinite loop. Typically, this means that you’re unconditionally dispatching an action during render, so your component enters a loop: render, dispatch (which causes a render), render, dispatch (which causes a render), and so on. Very often, this is caused by a mistake in specifying an event handler:

// 🚩 Wrong: calls the handler during render
return <button onClick={handleClick()}>Click me</button>

// ✅ Correct: passes down the event handler
return <button onClick={handleClick}>Click me</button>

// ✅ Correct: passes down an inline function
return <button onClick={(e) => handleClick(e)}>Click me</button>

如果你找不到此错误的原因,请点击控制台中错误旁边的箭头,并查看 JavaScript 堆栈,以找出导致错误的具体 dispatch 函数调用。

🌐 If you can’t find the cause of this error, click on the arrow next to the error in the console and look through the JavaScript stack to find the specific dispatch function call responsible for the error.


我的 reducer 或初始化函数运行两次

🌐 My reducer or initializer function runs twice

严格模式 下,React 会调用你的 reducer 和初始化函数两次。这不应该破坏你的代码。

🌐 In Strict Mode, React will call your reducer and initializer functions twice. This shouldn’t break your code.

这种仅在开发环境下的行为可以帮助你保持组件的纯粹性。React 会使用其中一次调用的结果,而忽略另一次调用的结果。只要你的组件、初始化函数和 reducer 函数是纯函数,这不会影响你的逻辑。然而,如果它们不小心变得不纯,这有助于你发现错误。

🌐 This development-only behavior helps you keep components pure. React uses the result of one of the calls, and ignores the result of the other call. As long as your component, initializer, and reducer functions are pure, this shouldn’t affect your logic. However, if they are accidentally impure, this helps you notice the mistakes.

例如,这个不纯的 reducer 函数会改变状态中的数组:

🌐 For example, this impure reducer function mutates an array in state:

function reducer(state, action) {
switch (action.type) {
case 'added_todo': {
// 🚩 Mistake: mutating state
state.todos.push({ id: nextId++, text: action.text });
return state;
}
// ...
}
}

因为 React 会调用你的 reducer 函数两次,你会看到 todo 被添加了两次,所以你就会知道有一个错误。在这个例子中,你可以通过替换数组而不是修改它来修复这个错误:

🌐 Because React calls your reducer function twice, you’ll see the todo was added twice, so you’ll know that there is a mistake. In this example, you can fix the mistake by replacing the array instead of mutating it:

function reducer(state, action) {
switch (action.type) {
case 'added_todo': {
// ✅ Correct: replacing with new state
return {
...state,
todos: [
...state.todos,
{ id: nextId++, text: action.text }
]
};
}
// ...
}
}

既然这个 reducer 函数是纯函数,额外调用一次它不会影响行为。这就是为什么 React 调用它两次可以帮助你发现错误。**只有组件、初始化函数和 reducer 函数需要是纯函数。**事件处理函数不需要是纯函数,所以 React 永远不会调用你的事件处理函数两次。

🌐 Now that this reducer function is pure, calling it an extra time doesn’t make a difference in behavior. This is why React calling it twice helps you find mistakes. Only component, initializer, and reducer functions need to be pure. Event handlers don’t need to be pure, so React will never call your event handlers twice.

阅读 保持组件纯粹 以了解更多信息。

🌐 Read keeping components pure to learn more.