跳到主要内容

迁移状态逻辑至 Reducer 中

  • 对于拥有许多状态更新逻辑的组件来说,过于分散的事件处理程序可能会令人不知所措。
  • 对于这种情况,你可以将组件的所有状态更新逻辑整合到一个外部函数中,这个函数叫作 reducer。

Reducer 是处理状态的另一种方式。

你可以通过三个步骤将 useState 迁移到 useReducer:

  1. 将设置状态的逻辑 修改dispatch 的一个 action
  2. 编写 一个 reducer 函数;
  3. 在你的组件中 使用 reducer

第 1 步: 将设置状态的逻辑修改成 dispatch 的一个 action

  • 你的事件处理程序目前是通过设置状态来 实现逻辑的:
  • 移除所有的状态设置逻辑。只留下三个事件处理函数:
    • handleAddTask(text) 在用户点击 “添加” 时被调用。
    • handleChangeTask(task) 在用户切换任务或点击 “保存” 时被调用。
    • handleDeleteTask(taskId) 在用户点击 “删除” 时被调用。
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))
}
  • 使用 reducers 管理状态与直接设置状态略有不同。
  • 它不是通过设置状态来告诉 React “要做什么”,而是通过事件处理程序 dispatch 一个 “action” 来指明 “用户刚刚做了什么”。
  • (而状态更新逻辑则保存在其他地方!)因此,我们不再通过事件处理器直接 “设置 task ”,
  • 而是 dispatch 一个 “添加/修改/删除任务” 的 action 。这更加符合用户的思维。

1. 通过事件处理程序dispatch 一个 “action” 要做什么?

通过dispatch发送action动作
function handleAddTask(text) {
dispatch(
// 一个action对象
{
type: 'added',
id: nextId++,
text: text,
}
)
}

function handleChangeTask(task) {
dispatch(
// 一个action对象
{
type: 'changed',
task: task,
}
)
}

function handleDeleteTask(taskId) {
dispatch(
// 一个action对象
{
type: 'deleted',
id: taskId,
}
)
}
  • action 对象:它是一个普通的 JavaScript 对象。它的结构是由你决定的,但通常来说,它应该至少包含可以表明 发生了什么事情 的信息。
  • action 对象可以有多种结构。
    • 按照惯例,我们通常会添加一个字符串类型的 type 字段来描述发生了什么,并通过其它字段传递额外的信息。
    • type 是特定于组件的,在这个例子中 added 和 addded_task 都可以。选一个能描述清楚发生的事件的名字!
dispatch({
// 针对特定的组件
type: 'what_happened',
// 其它字段放这里
})

第 2 步: 编写一个 reducer 函数

  • reducer 函数就是你放置状态逻辑的地方。
  • 接受:两个参数, stateaction 对象
  • 返回:更新后的 新state
function yourReducer(state, action) {
// 给 React 返回更新后的状态
}

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

  1. 声明当前状态(tasks)作为第一个参数;
  2. 声明 action 对象作为第二个参数;
  3. reducer 返回 下一个 状态( React 会将旧的状态设置为这个最新的状态)。
下面是所有迁移到 reducer 函数的状态设置逻辑:
// tasks 你的旧状态,action 动作
// 返回的是新状态,新的tasks
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('未知 action: ' + action.type)
}
}
}
// 我们建议将每个 case 块包装到 { 和 } 花括号中
  • 由于 reducer 函数接受 state(tasks)作为参数,因此你可以 在组件之外声明它。这减少了代码的缩进级别,提升了代码的可读性。

为什么称之为 reducer?

  • 尽管 reducer 可以 “减少” 组件内的代码量,但它实际上是以数组上的 reduce() 方法命名的。
  • reduce() 允许你将数组中的多个值 “累加” 成一个值:
reduce() 累加函数
const arr = [1, 2, 3, 4, 5]
const sum = arr.reduce((result, number) => result + number) // 1 + 2 + 3 + 4 + 5
  • 你传递给 reduce 的函数被称为 “reducer” 。
  • 它接受 目前的结果当前的值 ,然后返回 下一个结果

React 中的 reducer 和这个是一样的:

  • 它们都接受 目前的状态action ,然后返回 新的状态
  • 这样, action 会随着时间推移累积到状态中。

第 3 步: 在组件中使用 reducer

  • 你需要将 tasksReducer 导入到组件中。
记得先从 React 中导入 useReducer Hook:
import { useReducer } from 'react'
接下来,你就可以替换掉之前的 useState:
const [tasks, setTasks] = useState(initialTasks)
只需要像下面这样使用 useReducer:
// 提供:
// tasksReducer 事件处理函数合集
// initialTasks 初始化状态数据
// 获得:
// tasks 状态值
// dispatch 发送动作
const [tasks, dispatch] = useReducer(tasksReducer, initialTasks)

useReducer 和 useState 很相似——你必须给它传递一个初始状态,它会返回一个有状态的值和一个设置该状态的函数(在这个例子中就是 dispatch 函数)。但是,它们两个之间还是有点差异的。

useReducer 钩子接受 2 个参数

  • 一个 reducer 函数
  • 一个初始的 state

它返回如下内容

  • 一个有状态的值
  • 一个 dispatch 函数(用来 “派发” 用户操作给 reducer)

现在一切都准备就绪了!我们在这里把 reducer 定义在了组件的末尾:

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>布拉格的行程安排</h1>
<AddTask onAddTask={handleAddTask} />
<TaskList
tasks={tasks}
onChangeTask={handleChangeTask}
onDeleteTask={handleDeleteTask}
/>
</>
)
}

let nextId = 3
const initialTasks = [
{ id: 0, text: '参观卡夫卡博物馆', done: true },
{ id: 1, text: '看木偶戏', done: false },
{ id: 2, text: '打卡列侬墙', done: false },
]
把 reducer 移到一个单独的文件中:
export default 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('未知 action:' + action.type)
}
}
}

对比 useState 和 useReducer

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

  • 代码体积:
    • 使用 useState 时,通常一开始只需要编写少量代码。
    • 而 useReducer 必须提前编写 reducer 函数和需要调度的 actions。
    • 但是,当多个事件处理程序以相似的方式修改 state 时,useReducer 可以减少代码量。
  • 可读性:
    • 当状态更新逻辑足够简单时,useState 的可读性还行。
    • 但是,一旦逻辑变得复杂起来,它们会使组件变得臃肿且难以阅读。
    • 在这种情况下,useReducer 允许你将 状态更新逻辑事件处理程序 分离 开来。
  • 可调试性:
    • 当使用 useState 出现问题时, 你很难发现具体原因以及为什么。
    • 而使用 useReducer 时, 你可以在 reducer 函数中通过打印日志的方式来观察每个状态的更新,以及为什么要更新(来自哪个 action)。
    • 如果所有 action 都没问题,你就知道问题出在了 reducer 本身的逻辑中。
    • 然而,与使用 useState 相比,你必须单步执行更多的代码。
  • 可测试性:
    • reducer 是一个不依赖于组件的纯函数。
    • 这就意味着你可以单独对它进行测试。
    • 一般来说,我们最好是在真实环境中测试组件,但对于复杂的状态更新逻辑,针对特定的初始状态和 action,断言 reducer 返回的特定状态会很有帮助。
  • 个人偏好:
    • 并不是所有人都喜欢用 reducer,没关系,这是个人偏好问题。
    • 你可以随时在 useState 和 useReducer 之间切换,它们能做的事情是一样的!

如果你在修改某些组件状态时经常出现问题或者想给组件添加更多逻辑时,我们建议你还是使用 reducer。当然,你也不必整个项目都用 reducer,这是可以自由搭配的。你甚至可以在一个组件中同时使用 useState 和 useReducer。

编写一个好的 reducers

编写 reducers 时最好牢记以下两点:

  • reducers 必须是纯粹的。

    • 这一点和 状态更新函数 是相似的,reducers 在是在渲染时运行的!(actions 会排队直到下一次渲染)。
    • 这就意味着 reducers 必须纯净,即当输入相同时,输出也是相同的。
    • 它们不应该包含异步请求、定时器或者任何副作用(对组件外部有影响的操作)。
      • 我:那应该如何获得服务器的异步数据呢?
    • 它们应该以不可变值的方式去更新 对象 和 数组
  • 每个 action 都描述了一个单一的用户交互,即使它会引发数据的多个变化

    • 举个例子,如果用户在一个由 reducer 管理的表单(包含五个表单项)中点击了 重置按钮
    • 那么 dispatch 一个 reset_form 的 action
    • dispatch 五个 单独的 set_field 的 action 更加合理。
    • 如果你在一个 reducer 中打印了所有的 action 日志,那么这个日志应该是很清晰的,它能让你以某种步骤复现已发生的交互或响应。这对代码调试很有帮助!

使用 Immer 简化 reducers

  • 与在平常的 state 中 修改对象 和 数组 一样,你可以使用 Immer 这个库来简化 reducer
  • 在这里, useImmerReducer 让你可以通过 pusharr[i] = 来修改 state
安装 immer
pnpm add immer
pnpm add use-immer
import { useImmerReducer } from 'use-immer'
import AddTask from './AddTask.js'
import TaskList from './TaskList.js'

function tasksReducer(draft, action) {
switch (action.type) {
case 'added': {
draft.push({
id: action.id,
text: action.text,
done: false,
})
break
}
case 'changed': {
const index = draft.findIndex((t) => t.id === action.task.id)
draft[index] = action.task
break
}
case 'deleted': {
return draft.filter((t) => t.id !== action.id)
}
default: {
throw Error('未知 action:' + action.type)
}
}
}

export default function TaskApp() {
const [tasks, dispatch] = useImmerReducer(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>布拉格的行程安排</h1>
<AddTask onAddTask={handleAddTask} />
<TaskList
tasks={tasks}
onChangeTask={handleChangeTask}
onDeleteTask={handleDeleteTask}
/>
</>
)
}

let nextId = 3
const initialTasks = [
{ id: 0, text: '参观卡夫卡博物馆', done: true },
{ id: 1, text: '看木偶戏', done: false },
{ id: 2, text: '打卡列侬墙', done: false },
]
  • Reducers 应该是纯净的,所以它们不应该去修改 state
  • Immer 为你提供了一种特殊的 draft(草案,草稿) 对象,你可以通过它安全的修改 state
  • 在底层, Immer 会基于当前 state 创建一个副本。
  • 这就是为什么通过 useImmerReducer 来管理 reducers 时,可以修改第一个参数,且不需要返回一个新的 state 的原因。

摘要

  • 把 useState 转化为 useReducer:
    • 通过事件处理函数 dispatch actions;
    • 编写一个 reducer 函数,它接受传入的 state 和一个 action,并返回一个新的 state;
    • 使用 useReducer 替换 useState;
  • Reducers 可能需要你写更多的代码,但是这有利于代码的调试和测试。
  • Reducers 必须是纯净的。
  • 每个 action 都描述了一个单一的用户交互。
  • 使用 Immer 来帮助你在 reducer 里直接修改状态。

  • 请记住,action 的类型应该准确描述 “用户做了什么”,而不是 “你希望状态如何改变”。这使得以后添加更多特性变的容易。
  • reducer 必须是一个纯函数——它应该只计算下一个状态。而不应该 “做” 其它事情,包括向用户显示消息。这应该在事件处理程序中处理。