v19.2Latest

将状态逻辑提取到 Reducer 中

如果一个组件中有许多状态更新分散在许多事件处理函数中,可能会变得难以管理。对于这种情况,你可以将所有状态更新逻辑整合到组件外部的一个单一函数中,这个函数被称为reducer。

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

使用 Reducer 整合状态逻辑

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

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

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

  1. 设置状态改为派发动作。
  2. 编写一个 reducer 函数。
  3. 组件中使用该 reducer。

步骤 1:将设置状态改为派发动作

你的事件处理函数目前通过设置状态来指定要做什么

移除所有设置状态的逻辑。剩下的就是三个事件处理函数:

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

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

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

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

注意

动作对象可以是任何形状。

按照惯例,通常会为其指定一个字符串type 来描述发生了什么,并在其他字段中传递任何附加信息。type是针对特定组件的,因此在这个例子中,'added''added_task'都可以。选择一个能说明发生了什么的名字!

步骤 2:编写一个 Reducer 函数

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

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

要将状态设置逻辑从事件处理函数移动到本示例中的 reducer 函数,你需要:

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

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

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

注意

上面的代码使用了 if/else 语句,但在 reducer 内部使用switch 语句是一种惯例。结果是一样的,但 switch 语句可能更容易一目了然。

在本文档的其余部分,我们将像这样使用它们:

我们建议将每个 case 代码块用 {} 花括号包裹起来,这样在不同 case中声明的变量就不会相互冲突。此外,一个case 通常应该以 return结束。如果你忘记return,代码将“落入”下一个 case,这可能导致错误!

如果你还不熟悉 switch 语句,使用 if/else 是完全没问题的。

步骤 3:在组件中使用 reducer

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

然后你可以替换useState

useReducer,如下所示:

useReducerHook 与useState类似——你必须向其传递一个初始状态,它会返回一个有状态的值和一个设置状态的方法(在这里是 dispatch 函数)。但它略有不同。

useReducerHook 接收两个参数:

  1. 一个 reducer 函数
  2. 一个初始状态

它返回:

  1. 一个有状态的值
  2. 一个 dispatch 函数(用于将用户操作“派发”给 reducer)

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

如果你愿意,甚至可以将 reducer 移动到另一个文件中:

像这样分离关注点可以使组件逻辑更易于阅读。现在事件处理函数仅通过派发 action 来指定发生了什么,而 reducer 函数则决定状态如何更新以响应这些 action。

比较 useStateuseReducer

Reducer 并非没有缺点!以下是几种比较它们的方式:

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

如果你经常因为某个组件中不正确的状态更新而遇到错误,并希望为其代码引入更多结构,我们建议使用 reducer。你不必对所有事情都使用 reducers:可以自由地混合搭配!你甚至可以在同一个组件中同时使用useStateuseReducer

编写良好的 reducers

编写 reducers 时请记住以下两个技巧:

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

使用 Immer 编写简洁的 reducer

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

Reducer 必须是纯函数,因此它们不应改变状态。但 Immer 为你提供了一个特殊的draft 对象,可以安全地改变它。在底层,Immer 将根据你对 draft所做的更改创建状态的副本。这就是为什么由useImmerReducer管理的 reducer 可以改变其第一个参数并且不需要返回状态。

回顾

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

Try out some challenges

Challenge 1 of 4:Dispatch actions from event handlers #

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.

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.