useReducer

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

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

参考

useReducer(reducer, initialArg, init?)

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

import { useReducer } from 'react';

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

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

请参见下面的更多示例。

参数

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

返回值

useReducer 返回一个包含两个值的数组。

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

注意事项

  • useReducer 是一个 Hook,因此你只能在组件的顶层或你自己的 Hook 中调用它。你不能在循环或条件内部调用它。如果你需要这样做,请提取一个新的组件并将状态移入其中。
  • dispatch 函数具有稳定的标识,因此你经常会看到它从 Effect 依赖项中省略,但包含它不会导致 Effect 触发。如果代码检查器允许你在没有错误的情况下省略依赖项,那么这样做是安全的。了解更多关于移除 Effect 依赖项的信息。
  • 在严格模式下,React 将调用你的 reducer 和初始化函数两次,以帮助你查找意外的纯度问题。 这只在开发环境中发生,不会影响生产环境。如果你的 reducer 和初始化函数是纯函数(它们应该如此),则这不会影响你的逻辑。其中一个调用的结果将被忽略。

dispatch 函数

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

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

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

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

参数

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

返回值

dispatch 函数没有返回值。

注意事项

  • dispatch 函数只在下一次渲染时更新状态变量。如果你在调用 dispatch 函数后读取状态变量,你仍然会得到屏幕上在你调用之前显示的旧值

  • 如果通过 Object.is 比较确定你提供的新值与当前 state 相同,则 React 将跳过重新渲染组件及其子组件。这是一个优化。React 仍然可能需要在忽略结果之前调用你的组件,但这不应该影响你的代码。

  • React 批量更新状态。它在所有事件处理程序运行完毕并调用其 set 函数后更新屏幕。这可以防止在单个事件期间进行多次重新渲染。在极少数情况下,你需要强制 React 更早地更新屏幕(例如,为了访问 DOM),你可以使用 flushSync


使用

向组件添加 reducer

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

import { useReducer } from 'react';

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

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

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

  1. 这个状态变量的当前状态,最初设置为您提供的初始状态
  2. dispatch 函数,它允许您响应交互来更改状态。

要更新屏幕上的内容,请使用表示用户操作的对象(称为*action*)调用dispatch

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

React 将把当前状态和 action 传递给您的reducer 函数。您的 reducer 将计算并返回下一个状态。React 将存储下一个状态,使用它渲染您的组件并更新 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>
    </>
  );
}

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


编写 reducer 函数

reducer 函数声明如下:

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

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

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

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

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

action 类型名称是您组件的局部变量。每个 action 描述单个交互,即使这会导致数据中的多个更改。 状态的形状是任意的,但通常它是一个对象或数组。

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

陷阱

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

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 返回新对象。

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

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

基本的 useReducer 示例

示例 1 3:
表单 (对象)

在这个例子中,reducer 管理一个状态对象,它有两个字段: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>
    </>
  );
}


避免重新创建初始状态

React 只保存一次初始状态,并在后续渲染中忽略它。

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

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

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

为了解决这个问题,您可以将它作为初始化函数传递给 useReducer 作为第三个参数。

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

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

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

在上面的示例中,createInitialState 获取一个 username 参数。如果您的初始化程序不需要任何信息来计算初始状态,您可以将 null 作为第二个参数传递给 useReducer

传递初始化器和直接传递初始状态的区别

示例 1 2:
传递初始化函数

此示例传递了初始化函数,因此createInitialState函数仅在初始化期间运行。当组件重新渲染时(例如,您在输入框中键入内容时),它不会运行。

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


故障排除

我已分派了一个动作,但日志显示的是旧的状态值

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

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 变量。

如果您需要猜测下一个状态值,您可以通过自己调用 reducer 来手动计算它。

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

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

我已分派了一个动作,但屏幕没有更新

React 将忽略您的更新,如果下一个状态等于前一个状态,这由Object.is 比较确定。这通常发生在您直接更改状态中的对象或数组时。

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 忽略了更新。要解决此问题,您需要确保始终更新状态中的对象更新状态中的数组,而不是修改它们。

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 状态的一部分变为未定义

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

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

如果没有上面...state,则返回的下一个状态将仅包含age字段,而不会包含其他任何内容。


分派后,我的整个 reducer 状态变为未定义

如果您的状态意外地变为undefined,您可能忘记在其中一个 case 中return state,或者您的 action 类型与任何case语句都不匹配。要查找原因,请在switch之外抛出错误。

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

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


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

你可能会遇到一个错误提示:重渲染次数过多。React 限制了渲染次数以防止无限循环。 通常情况下,这意味着你在渲染期间无条件地分发了 action,导致你的组件进入循环:渲染、分发(导致渲染)、渲染、分发(导致渲染),依此类推。很多时候,这是由事件处理程序的指定错误引起的。

// 🚩 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函数调用。


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

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

仅限开发环境的行为可以帮助你保持组件的纯净。React 使用其中一个调用的结果,并忽略另一个调用的结果。只要你的组件、初始化程序和 reducer 函数是纯函数,这就不应该影响你的逻辑。但是,如果它们意外地不纯,这将帮助你发现错误。

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

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 函数两次,你将看到待办事项被添加了两次,因此你会知道存在错误。在这个例子中,你可以通过替换数组而不是更改它来修复错误。

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 永远不会调用你的事件处理程序两次。

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