对于拥有许多状态更新逻辑的组件来说,过于分散的事件处理程序可能会令人不知所措。对于这种情况,可以将组件的所有状态更新逻辑整合到一个外部函数中,这个函数叫作 reducer。
随着组件复杂度的增加,将很难一眼看清所有的组件状态更新逻辑。例如,下面的 TaskApp
组件有一个数组类型的状态 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>布拉格的行程安排</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},
];
这个组件的每个事件处理程序都通过 setTasks
来更新状态。随着这个组件的不断迭代,其状态逻辑也会越来越多。为了降低这种复杂度,并让所有逻辑都可以存放在一个易于理解的地方,可以将这些状态逻辑移到组件之外的一个称为 reducer 的函数中。
Reducer 是处理状态的另一种方式。可以通过三个步骤将 useState
迁移到 useReducer
:
事件处理程序目前是通过设置状态来 实现逻辑的:
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));
}
移除所有的状态设置逻辑。只留下三个事件处理函数:
handleAddTask(text)
在用户点击 “添加” 时被调用。handleChangeTask(task)
在用户切换任务或点击 “保存” 时被调用。handleDeleteTask(taskId)
在用户点击 “删除” 时被调用。使用 reducers 管理状态与直接设置状态略有不同。它不是通过设置状态来告诉 React “要做什么”,而是通过事件处理程序 dispatch 一个 “action” 来指明 “用户刚刚做了什么”。(而状态更新逻辑则保存在其他地方!)因此,不再通过事件处理器直接 “设置 task
”,而是 dispatch 一个 “添加/修改/删除任务” 的 action。这更加符合用户的思维。
function handleAddTask(text) {
dispatch({
type: 'added',
id: nextId++,
text: text,
});
}
function handleChangeTask(task) {
dispatch({
type: 'changed',
task: task,
});
}
传递给 dispatch
的对象叫做 “action”:
function handleDeleteTask(taskId) {
dispatch(
// "action" 对象:
{
type: 'deleted',
id: taskId,
}
);
}
它是一个普通的 JavaScript 对象。它的结构是由你决定的,但通常来说,它应该至少包含可以表明 发生了什么事情 的信息。
注意:action 对象可以有多种结构。
按照惯例,通常会添加一个字符串类型的 type
字段来描述发生了什么,并通过其它字段传递额外的信息。type
是特定于组件的,在这个例子中 added
和 addded_task
都可以。选一个能描述清楚发生的事件的名字!
dispatch({
// 针对特定的组件
type: 'what_happened',
// 其它字段放这里
});
reducer 函数就是放置状态逻辑的地方。它接受两个参数,分别为当前 state 和 action 对象,并且返回的是更新后的 state:
function yourReducer(state, action) {
// 给 React 返回更新后的状态
}
React 会将状态设置为从 reducer 返回的状态。
在这个例子中,要将状态设置逻辑从事件处理程序移到 reducer 函数中,需要:
tasks
)作为第一个参数;action
对象作为第二个参数;reducer
返回 下一个 状态(React 会将旧的状态设置为这个最新的状态)。下面是所有迁移到 reducer
函数的状态设置逻辑:
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('未知 action: ' + action.type);
}
}
由于 reducer
函数接受 state
(tasks)作为参数,因此可以 在组件之外声明它。这减少了代码的缩进级别,提升了代码的可读性。
注意:上面的代码使用了 if/else
语句,但是在 reducers 中使用 switch 语句 是一种惯例。两种方式结果是相同的,但 switch
语句读起来一目了然。
以后会像这样使用:
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
块包装到 {
和 }
花括号中,这样在不同 case
中声明的变量就不会互相冲突。此外,case
通常应该以 return
结尾。如果忘了 return
,代码就会 进入
到下一个 case
,这就会导致错误!
如果还不熟悉 switch
语句,使用 if/else
也是可以的。
最后,需要将 tasksReducer
导入到组件中。记得先从 React 中导入 useReducer
Hook:
import { useReducer } from 'react';
接下来,就可以替换掉之前的 useState
:
const [tasks, setTasks] = useState(initialTasks);
只需要像下面这样使用 useReducer
:
const [tasks, dispatch] = useReducer(tasksReducer, initialTasks);
useReducer
和 useState
很相似——必须给它传递一个初始状态,它会返回一个有状态的值和一个设置该状态的函数(在这个例子中就是 dispatch 函数)。但是,它们两个之间还是有点差异的。
useReducer
钩子接受 2 个参数:
它返回如下内容:
现在一切都准备就绪了!在这里把 reducer 定义在了组件的末尾:
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>布拉格的行程安排</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('未知 action: ' + action.type);
}
}
}
...
如果有需要,甚至可以把 reducer 移到一个单独的文件中:
// tasksReducer.js
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);
}
}
}
当像这样分离关注点时,我们可以更容易地理解组件逻辑。现在,事件处理程序只通过派发 action
来指定 发生了什么,而 reducer
函数通过响应 actions
来决定 状态如何更新。
useState
和 useReducer
Reducers 并非没有缺点!以下是比较它们的几种方法:
useState
时,一开始只需要编写少量代码。而 useReducer
必须提前编写 reducer 函数和需要调度的 actions。但是,当多个事件处理程序以相似的方式修改 state 时,useReducer
可以减少代码量。useState
的可读性还行。但是,一旦逻辑变得复杂起来,它们会使组件变得臃肿且难以阅读。在这种情况下,useReducer
允许将状态更新逻辑与事件处理程序分离开来。useState
出现问题时, 很难发现具体原因以及为什么。 而使用 useReducer
时, 可以在 reducer 函数中通过打印日志的方式来观察每个状态的更新,以及为什么要更新(来自哪个 action
)。 如果所有 action
都没问题,就知道问题出在了 reducer 本身的逻辑中。 然而,与使用 useState
相比,必须单步执行更多的代码。action
,断言 reducer 返回的特定状态会很有帮助。useState
和 useReducer
之间切换,它们能做的事情是一样的!如果在修改某些组件状态时经常出现问题或者想给组件添加更多逻辑时,建议还是使用 reducer。当然,也不必整个项目都用 reducer,这是可以自由搭配的。甚至可以在一个组件中同时使用 useState
和 useReducer
。
编写 reducers
时最好牢记以下两点:
reducers
在是在渲染时运行的!(actions 会排队直到下一次渲染)。 这就意味着 reducers
必须纯净,即当输入相同时,输出也是相同的。它们不应该包含异步请求、定时器或者任何副作用(对组件外部有影响的操作)。它们应该以不可变值的方式去更新 对象 和 数组。reducer
管理的表单(包含五个表单项)中点击了 重置按钮
,那么 dispatch 一个 reset_form
的 action 比 dispatch 五个单独的 set_field
的 action 更加合理。如果在一个 reducer
中打印了所有的 action
日志,那么这个日志应该是很清晰的,它能让你以某种步骤复现已发生的交互或响应。这对代码调试很有帮助!与在平常的 state 中 修改对象 和 数组 一样,可以使用 Immer
这个库来简化 reducer
。在这里,useImmerReducer
让你可以通过 push
或 arr[i] =
来修改 state :
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}
/>
</>
);
}
...
Reducers 应该是纯净的,所以它们不应该去修改 state。而 Immer 提供了一种特殊的 draft
对象,可以通过它安全的修改 state。在底层,Immer 会基于当前 state 创建一个副本。这就是为什么通过 useImmerReducer
来管理 reducers 时,可以修改第一个参数,且不需要返回一个新的 state 的原因。
useState
转化为 useReducer
:
useReducer
替换 useState
;