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:从中计算初始状态的值。它可以是任何类型的值。如何从中计算初始状态取决于下一个 init 参数。
  • 可选 init:应该返回初始状态的初始化器函数。如果没有指定,则初始状态设置为 initialArg。否则,初始状态设置为调用 init(initialArg) 的结果。

返回值

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

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

注意事项

  • useReducer 是一个 Hook,所以你只能在组件的顶层或你自己的 Hook 中调用它。 你不能在循环或条件语句中调用它。 如果你需要这样做,请提取一个新的组件并将状态移到其中。
  • 在严格模式下,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 }

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

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

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 状态的一部分在分派后变为 undefined

确保每个 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

如果您的状态意外变为 undefined,则可能是您忘记在其中一种情况下 return 状态,或者您的操作类型与任何 case 语句都不匹配。要找出原因,请在 switch 之外抛出错误

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

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


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

您可能会收到一条错误消息:渲染次数过多。React 限制渲染次数以防止无限循环。 这通常意味着您在_渲染期间_无条件地分派了一个操作,因此您的组件进入了一个循环:渲染、分派(导致渲染)、渲染、分派(导致渲染),依此类推。很多时候,这是由于在指定事件处理程序时出错造成的

// 🚩 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 或初始化函数运行了两次

严格模式下,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 永远不会调用您的事件处理程序两次。

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