将状态逻辑提取到 Reducer 中

具有许多状态更新并分散在多个事件处理程序中的组件可能会让人应接不暇。对于这些情况,你可以将所有状态更新逻辑集中在组件外的一个函数中,称为 reducer

🌐 Components with many state updates spread across many event handlers can get overwhelming. For these cases, you can consolidate all the state update logic outside your component in a single function, called a reducer.

你将学习到

  • 什么是 reducer 函数
  • 如何将 useState 重构为 useReducer
  • 什么时候使用 reducer
  • 如何编写好一个

使用 reducer 整合状态逻辑

🌐 Consolidate state logic with a reducer

随着你的组件复杂性增加,一眼就看清组件状态更新的所有不同方式可能会变得更加困难。例如,下面的 TaskApp 组件在状态中保存了一个 tasks 数组,并使用三个不同的事件处理函数来添加、移除和编辑任务:

🌐 As your components grow in complexity, it can get harder to see at a glance all the different ways in which a component’s state gets updated. For example, the TaskApp component below holds an array of tasks in state and uses three different event handlers to add, remove, and edit tasks:

import { useState } from 'react';
import AddTask from './AddTask.js';
import TaskList from './TaskList.js';

export default function TaskApp() {
  const [tasks, setTasks] = useState(initialTasks);

  function handleAddTask(text) {
    setTasks([
      ...tasks,
      {
        id: nextId++,
        text: text,
        done: false,
      },
    ]);
  }

  function handleChangeTask(task) {
    setTasks(
      tasks.map((t) => {
        if (t.id === task.id) {
          return task;
        } else {
          return t;
        }
      })
    );
  }

  function handleDeleteTask(taskId) {
    setTasks(tasks.filter((t) => t.id !== taskId));
  }

  return (
    <>
      <h1>Prague itinerary</h1>
      <AddTask onAddTask={handleAddTask} />
      <TaskList
        tasks={tasks}
        onChangeTask={handleChangeTask}
        onDeleteTask={handleDeleteTask}
      />
    </>
  );
}

let nextId = 3;
const initialTasks = [
  {id: 0, text: 'Visit Kafka Museum', done: true},
  {id: 1, text: 'Watch a puppet show', done: false},
  {id: 2, text: 'Lennon Wall pic', done: false},
];

它的每个事件处理程序都会调用 setTasks 来更新状态。随着该组件的增长,分散在其中的状态逻辑也会增加。为了减少这种复杂性,并将所有逻辑集中到一个易于访问的地方,你可以将这些状态逻辑移到组件外的单个函数中,称为“reducer”。

🌐 Each of its event handlers calls setTasks in order to update the state. As this component grows, so does the amount of state logic sprinkled throughout it. To reduce this complexity and keep all your logic in one easy-to-access place, you can move that state logic into a single function outside your component, called a “reducer”.

Reducers 是处理状态的另一种方式。你可以通过三步将 useState 迁移到 useReducer

🌐 Reducers are a different way to handle state. You can migrate from useState to useReducer in three steps:

  1. 设置状态转向分发动作。
  2. 编写一个 reducer 函数。
  3. 使用 你组件中的 reducer。

步骤 1:从设置状态转换为分发动作

🌐 Step 1: Move from setting state to dispatching actions

你的事件处理程序当前通过设置状态来指定_要做什么_:

🌐 Your event handlers currently specify what to do by setting state:

function handleAddTask(text) {
setTasks([
...tasks,
{
id: nextId++,
text: text,
done: false,
},
]);
}

function handleChangeTask(task) {
setTasks(
tasks.map((t) => {
if (t.id === task.id) {
return task;
} else {
return t;
}
})
);
}

function handleDeleteTask(taskId) {
setTasks(tasks.filter((t) => t.id !== taskId));
}

删除所有状态设置逻辑。剩下的是三个事件处理程序:

🌐 Remove all the state setting logic. What you are left with are three event handlers:

  • 当用户按下“添加”时,会调用 handleAddTask(text)
  • handleChangeTask(task) 在用户切换任务或按下“保存”时被调用。
  • handleDeleteTask(taskId) 在用户按下“删除”时被调用。

使用 reducers 管理状态与直接设置状态略有不同。你不是通过设置状态来告诉 React “要做什么”,而是通过从你的事件处理程序派发“操作”来指定“用户刚刚做了什么”。(状态更新逻辑将在其他地方!)所以,你不是通过事件处理程序“设置 tasks”,而是在派发一个“添加/修改/删除了一个任务”的操作。这更能描述用户的意图。

🌐 Managing state with reducers is slightly different from directly setting state. Instead of telling React “what to do” by setting state, you specify “what the user just did” by dispatching “actions” from your event handlers. (The state update logic will live elsewhere!) So instead of “setting tasks” via an event handler, you’re dispatching an “added/changed/deleted a task” action. This is more descriptive of the user’s intent.

function handleAddTask(text) {
dispatch({
type: 'added',
id: nextId++,
text: text,
});
}

function handleChangeTask(task) {
dispatch({
type: 'changed',
task: task,
});
}

function handleDeleteTask(taskId) {
dispatch({
type: 'deleted',
id: taskId,
});
}

你传递给 dispatch 的对象被称为“动作”

🌐 The object you pass to dispatch is called an “action”:

function handleDeleteTask(taskId) {
dispatch(
// "action" object:
{
type: 'deleted',
id: taskId,
}
);
}

它是一个常规的 JavaScript 对象。你可以决定放入什么内容,但通常它应该包含关于“发生了什么”的最少信息。(你将在后续步骤中添加 dispatch 函数本身。)

🌐 It is a regular JavaScript object. You decide what to put in it, but generally it should contain the minimal information about what happened. (You will add the dispatch function itself in a later step.)

注意

一个操作对象可以有任何形状。

🌐 An action object can have any shape.

按照惯例,通常会给它一个描述发生了什么的字符串 type,并将任何额外信息传递到其他字段中。type 是特定于某个组件的,因此在这个例子中 'added''added_task' 都可以。选择一个能够说明发生了什么的名字!

🌐 By convention, it is common to give it a string type that describes what happened, and pass any additional information in other fields. The type is specific to a component, so in this example either 'added' or 'added_task' would be fine. Choose a name that says what happened!

dispatch({
// specific to component
type: 'what_happened',
// other fields go here
});

步骤 2:编写一个reducer函数

🌐 Step 2: Write a reducer function

reducer 函数是你放置状态逻辑的地方。它接受两个参数,当前状态和动作对象,并返回下一个状态:

🌐 A reducer function is where you will put your state logic. It takes two arguments, the current state and the action object, and it returns the next state:

function yourReducer(state, action) {
// return next state for React to set
}

React 会将状态设置为你从 reducer 返回的内容。

🌐 React will set the state to what you return from the reducer.

在此示例中,要将状态设置逻辑从事件处理程序移动到 reducer 函数,你将:

🌐 To move your state setting logic from your event handlers to a reducer function in this example, you will:

  1. 将当前状态(tasks)声明为第一个参数。
  2. action 对象声明为第二个参数。
  3. 从 reducer 返回 下一个 状态(React 将把状态设置为这个状态)。

以下是迁移到 reducer 函数的所有状态设置逻辑:

🌐 Here is all the state setting logic migrated to a reducer function:

function tasksReducer(tasks, action) {
if (action.type === 'added') {
return [
...tasks,
{
id: action.id,
text: action.text,
done: false,
},
];
} else if (action.type === 'changed') {
return tasks.map((t) => {
if (t.id === action.task.id) {
return action.task;
} else {
return t;
}
});
} else if (action.type === 'deleted') {
return tasks.filter((t) => t.id !== action.id);
} else {
throw Error('Unknown action: ' + action.type);
}
}

因为 reducer 函数将状态(tasks)作为参数,所以你可以在组件外声明它。 这可以减少缩进层级,并使你的代码更易于阅读。

🌐 Because the reducer function takes state (tasks) as an argument, you can declare it outside of your component. This decreases the indentation level and can make your code easier to read.

注意

上面的代码使用了 if/else 语句,但在 reducers 内部使用 switch 语句 是一种惯例。结果是相同的,但从一眼看过去,switch 语句可能更容易阅读。

🌐 The code above uses if/else statements, but it’s a convention to use switch statements inside reducers. The result is the same, but it can be easier to read switch statements at a glance.

我们将在本文档的其余部分使用它们,如下所示:

🌐 We’ll be using them throughout the rest of this documentation like so:

function tasksReducer(tasks, action) {
switch (action.type) {
case 'added': {
return [
...tasks,
{
id: action.id,
text: action.text,
done: false,
},
];
}
case 'changed': {
return tasks.map((t) => {
if (t.id === action.task.id) {
return action.task;
} else {
return t;
}
});
}
case 'deleted': {
return tasks.filter((t) => t.id !== action.id);
}
default: {
throw Error('Unknown action: ' + action.type);
}
}
}

我们建议将每个 case 块封装在 {} 大括号中,这样在不同 case 中声明的变量就不会相互冲突。此外,case 通常应该以 return 结尾。如果你忘记 return,代码将会“穿透”到下一个 case,这可能会导致错误!

🌐 We recommend wrapping each case block into the { and } curly braces so that variables declared inside of different cases don’t clash with each other. Also, a case should usually end with a return. If you forget to return, the code will “fall through” to the next case, which can lead to mistakes!

如果你还不习惯 switch 语句,那么使用 if/else 完全没问题。

🌐 If you’re not yet comfortable with switch statements, using if/else is completely fine.

深入研究

为什么 reducer 这么调用?

🌐 Why are reducers called this way?

虽然 reducers 可以“减少”组件内部的代码量,但它们实际上是以你可以在数组上执行的 reduce() 操作命名的。

🌐 Although reducers can “reduce” the amount of code inside your component, they are actually named after the reduce() operation that you can perform on arrays.

reduce() 操作允许你从一个数组中“累积”出一个单一的值:

🌐 The reduce() operation lets you take an array and “accumulate” a single value out of many:

const arr = [1, 2, 3, 4, 5];
const sum = arr.reduce(
(result, number) => result + number
); // 1 + 2 + 3 + 4 + 5

你传给 reduce 的函数被称为“reducer”。它接受_到目前为止的结果_和_当前项_,然后返回_下一个结果_。React 中的 reducers 就是相同理念的一个例子:它们接受_到目前为止的状态_和_动作_,并返回_下一个状态_。通过这种方式,它们随着时间的推移将动作累积到状态中。

🌐 The function you pass to reduce is known as a “reducer”. It takes the result so far and the current item, then it returns the next result. React reducers are an example of the same idea: they take the state so far and the action, and return the next state. In this way, they accumulate actions over time into state.

你甚至可以使用 reduce() 方法与一个 initialState 和一个 actions 数组,通过将你的 reducer 函数传递给它来计算最终状态:

🌐 You could even use the reduce() method with an initialState and an array of actions to calculate the final state by passing your reducer function to it:

import tasksReducer from './tasksReducer.js';

let initialState = [];
let actions = [
  {type: 'added', id: 1, text: 'Visit Kafka Museum'},
  {type: 'added', id: 2, text: 'Watch a puppet show'},
  {type: 'deleted', id: 1},
  {type: 'added', id: 3, text: 'Lennon Wall pic'},
];

let finalState = actions.reduce(tasksReducer, initialState);

const output = document.getElementById('output');
output.textContent = JSON.stringify(finalState, null, 2);

你可能不需要自己执行此操作,但这与 React 所做的类似!

🌐 You probably won’t need to do this yourself, but this is similar to what React does!

步骤 3:使用你组件中的 reducer

🌐 Step 3: Use the reducer from your component

最后,你需要将 tasksReducer 连接到你的组件。从 React 导入 useReducer Hook:

🌐 Finally, you need to hook up the tasksReducer to your component. Import the useReducer Hook from React:

import { useReducer } from 'react';

然后你可以替换 useState

🌐 Then you can replace useState:

const [tasks, setTasks] = useState(initialTasks);

像这样使用 useReducer

🌐 with useReducer like so:

const [tasks, dispatch] = useReducer(tasksReducer, initialTasks);

useReducer Hook 类似于 useState——你必须传入一个初始状态,它会返回一个有状态的值和一个设置状态的方法(在这种情况下,是 dispatch 函数)。但它有一些不同。

🌐 The useReducer Hook is similar to useState—you must pass it an initial state and it returns a stateful value and a way to set state (in this case, the dispatch function). But it’s a little different.

useReducer 钩子接收两个参数:

🌐 The useReducer Hook takes two arguments:

  1. reducer 函数
  2. 初始状态

它返回:

🌐 And it returns:

  1. 有状态的值
  2. 一个派发函数(用于将用户操作“派发”到归约器)

现在它已经完全连接好了!这里,reducer 在组件文件的底部声明:

🌐 Now it’s fully wired up! Here, the reducer is declared at the bottom of the component file:

import { useReducer } from 'react';
import AddTask from './AddTask.js';
import TaskList from './TaskList.js';

export default function TaskApp() {
  const [tasks, dispatch] = useReducer(tasksReducer, initialTasks);

  function handleAddTask(text) {
    dispatch({
      type: 'added',
      id: nextId++,
      text: text,
    });
  }

  function handleChangeTask(task) {
    dispatch({
      type: 'changed',
      task: task,
    });
  }

  function handleDeleteTask(taskId) {
    dispatch({
      type: 'deleted',
      id: taskId,
    });
  }

  return (
    <>
      <h1>Prague itinerary</h1>
      <AddTask onAddTask={handleAddTask} />
      <TaskList
        tasks={tasks}
        onChangeTask={handleChangeTask}
        onDeleteTask={handleDeleteTask}
      />
    </>
  );
}

function tasksReducer(tasks, action) {
  switch (action.type) {
    case 'added': {
      return [
        ...tasks,
        {
          id: action.id,
          text: action.text,
          done: false,
        },
      ];
    }
    case 'changed': {
      return tasks.map((t) => {
        if (t.id === action.task.id) {
          return action.task;
        } else {
          return t;
        }
      });
    }
    case 'deleted': {
      return tasks.filter((t) => t.id !== action.id);
    }
    default: {
      throw Error('Unknown action: ' + action.type);
    }
  }
}

let nextId = 3;
const initialTasks = [
  {id: 0, text: 'Visit Kafka Museum', done: true},
  {id: 1, text: 'Watch a puppet show', done: false},
  {id: 2, text: 'Lennon Wall pic', done: false},
];

如果你愿意,你甚至可以将 reducer 移动到不同的文件:

🌐 If you want, you can even move the reducer to a different file:

import { useReducer } from 'react';
import AddTask from './AddTask.js';
import TaskList from './TaskList.js';
import tasksReducer from './tasksReducer.js';

export default function TaskApp() {
  const [tasks, dispatch] = useReducer(tasksReducer, initialTasks);

  function handleAddTask(text) {
    dispatch({
      type: 'added',
      id: nextId++,
      text: text,
    });
  }

  function handleChangeTask(task) {
    dispatch({
      type: 'changed',
      task: task,
    });
  }

  function handleDeleteTask(taskId) {
    dispatch({
      type: 'deleted',
      id: taskId,
    });
  }

  return (
    <>
      <h1>Prague itinerary</h1>
      <AddTask onAddTask={handleAddTask} />
      <TaskList
        tasks={tasks}
        onChangeTask={handleChangeTask}
        onDeleteTask={handleDeleteTask}
      />
    </>
  );
}

let nextId = 3;
const initialTasks = [
  {id: 0, text: 'Visit Kafka Museum', done: true},
  {id: 1, text: 'Watch a puppet show', done: false},
  {id: 2, text: 'Lennon Wall pic', done: false},
];

当你像这样分离关注点时,组件逻辑会更容易阅读。现在,事件处理程序只通过派发动作来指定“发生了什么”,而 reducer 函数则决定如何根据这些动作更新状态。

🌐 Component logic can be easier to read when you separate concerns like this. Now the event handlers only specify what happened by dispatching actions, and the reducer function determines how the state updates in response to them.

比较 useStateuseReducer

🌐 Comparing useState and useReducer

Reducer 并非没有缺点!这里有几种比较它们的方法:

🌐 Reducers are not without downsides! Here’s a few ways you can compare them:

  • 代码量: 通常,使用 useState 时,你需要在一开始写的代码较少。使用 useReducer 时,你需要编写 reducer 函数 以及 分发动作。然而,如果许多事件处理程序以类似的方式修改状态,useReducer 可以帮助减少代码量。
  • 可读性: 当状态更新很简单时,useState 非常容易阅读。当它们变得更复杂时,可能会使组件的代码膨胀,并且难以浏览。在这种情况下,useReducer 让你可以干净地将更新逻辑的 如何 与事件处理程序的 发生了什么 分开。
  • 调试: 当你在使用 useState 时遇到 bug,很难判断状态是 在哪里 设置错误的,以及 为什么 出现错误。使用 useReducer,你可以在 reducer 中添加控制台日志来查看每一次状态更新,以及它 为什么 发生(是由于哪个 action)。如果每个 action 都是正确的,你就会知道错误在于 reducer 的逻辑本身。然而,你需要比使用 useState 时更多地逐步调试代码。
  • 测试: reducer 是一个纯函数,不依赖你的组件。这意味着你可以单独导出并在隔离的环境中测试它。虽然通常最好在更真实的环境中测试组件,但对于复杂的状态更新逻辑,断言你的 reducer 对特定的初始状态和操作返回特定状态可能是有用的。
  • 个人偏好: 有些人喜欢使用 reducer,有些人不喜欢。这没关系。这是偏好的问题。你总是可以在 useStateuseReducer 之间来回转换:它们是等价的!

如果你经常在某些组件中由于状态更新不正确而遇到错误,并且希望为其代码引入更多结构,我们建议使用 reducer。你不必为所有内容都使用 reducer:可以随意混合使用!你甚至可以在同一个组件中 useStateuseReducer

🌐 We recommend using a reducer if you often encounter bugs due to incorrect state updates in some component, and want to introduce more structure to its code. You don’t have to use reducers for everything: feel free to mix and match! You can even useState and useReducer in the same component.

写好 reducer

🌐 Writing reducers well

编写 reducer 时请牢记这两个提示:

🌐 Keep these two tips in mind when writing reducers:

  • Reducer 必须是纯函数。 类似于 状态更新函数,reducer 会在渲染期间运行!(动作会排队,直到下一次渲染。)这意味着 reducer 必须是纯函数—相同的输入总会产生相同的输出。它们不应该发送请求、安排超时或执行任何副作用(影响组件外部的操作)。它们应该在不发生修改的情况下更新 对象数组
  • 每个动作描述一次单独的用户交互,即使这会导致数据发生多次更改。 例如,如果用户在一个有五个字段的表单上按下“重置”,由 reducer 管理的话,派发一个 reset_form 动作比派发五个单独的 set_field 动作更合理。如果你在 reducer 中记录每个动作,这个日志应该足够清晰,让你能够重建交互或响应发生的顺序。这有助于调试!

使用 Immer 编写简洁的 reducer

🌐 Writing concise reducers with Immer

就像在常规状态中更新对象数组一样,你可以使用 Immer 库使 reducers 更简洁。在这里,useImmerReducer 允许你使用 pusharr[i] = 赋值来修改状态:

🌐 Just like with updating objects and arrays in regular state, you can use the Immer library to make reducers more concise. Here, useImmerReducer lets you mutate the state with push or arr[i] = assignment:

{
  "dependencies": {
    "immer": "1.7.3",
    "react": "latest",
    "react-dom": "latest",
    "react-scripts": "latest",
    "use-immer": "0.5.1"
  },
  "scripts": {
    "start": "react-scripts start",
    "build": "react-scripts build",
    "test": "react-scripts test --env=jsdom",
    "eject": "react-scripts eject"
  },
  "devDependencies": {}
}

Reducer 必须是纯函数,因此它们不应改变 state。但 Immer 提供了一个特殊的 draft 对象,可以安全地修改。在底层,Immer 会创建一个包含你对 draft 所做修改的 state 副本。这就是为什么由 useImmerReducer 管理的 reducer 可以修改它的第一个参数,而不需要返回 state 的原因。

🌐 Reducers must be pure, so they shouldn’t mutate state. But Immer provides you with a special draft object which is safe to mutate. Under the hood, Immer will create a copy of your state with the changes you made to the draft. This is why reducers managed by useImmerReducer can mutate their first argument and don’t need to return state.

回顾

  • 要将 useState 转换为 useReducer
    1. 从事件处理程序调度操作。
    2. 编写一个 reducer 函数,返回给定状态和操作的下一个状态。
    3. useState 替换为 useReducer
  • Reducer 需要你编写更多代码,但它们有助于调试和测试。
  • Reducer 必须是纯粹的。
  • 每个操作都描述了一次用户交互。
  • 如果你想以可变风格编写 reducer,请使用 Immer。

挑战 1 of 4:
从事件处理程序调度操作

🌐 Dispatch actions from event handlers

目前,ContactList.jsChat.js 中的事件处理程序都有 // TODO 注释。这就是为什么输入框无法输入,以及点击按钮不会更改选定的收件人。

🌐 Currently, the event handlers in ContactList.js and Chat.js have // TODO comments. This is why typing into the input doesn’t work, and clicking on the buttons doesn’t change the selected recipient.

将这两个 // TODO 替换为用于 dispatch 相应操作的代码。要查看操作的预期形状和类型,请检查 messengerReducer.js 中的 reducer。reducer 已经写好,因此你不需要修改它。你只需要在 ContactList.jsChat.js 中派发这些操作。

🌐 Replace these two // TODOs with the code to dispatch the corresponding actions. To see the expected shape and the type of the actions, check the reducer in messengerReducer.js. The reducer is already written so you won’t need to change it. You only need to dispatch the actions in ContactList.js and Chat.js.

import { useReducer } from 'react';
import Chat from './Chat.js';
import ContactList from './ContactList.js';
import { initialState, messengerReducer } from './messengerReducer';

export default function Messenger() {
  const [state, dispatch] = useReducer(messengerReducer, initialState);
  const message = state.message;
  const contact = contacts.find((c) => c.id === state.selectedId);
  return (
    <div>
      <ContactList
        contacts={contacts}
        selectedId={state.selectedId}
        dispatch={dispatch}
      />
      <Chat
        key={contact.id}
        message={message}
        contact={contact}
        dispatch={dispatch}
      />
    </div>
  );
}

const contacts = [
  {id: 0, name: 'Taylor', email: 'taylor@mail.com'},
  {id: 1, name: 'Alice', email: 'alice@mail.com'},
  {id: 2, name: 'Bob', email: 'bob@mail.com'},
];