许多state更新逻辑分布在多个事件处理函数中,会使组件变的很大和难以维护。针对这个问题,把所有的state逻辑合并到一个reducer函数中,且这个reducer函数在组件外。
用一个reducer函数合并state逻辑
随着组件复杂性的增长,要想一目了然地看到组件所有的不同的state更新逻辑可能会变得越来越困难。例如,下面的TaskApp组件把任务列表放到存储在state的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来更新state。随着这个组件的功能增加,散布在其中的state更新逻辑的数量也在增长。为了减少这种复杂性,把所有state更新逻辑保存在一个易于管理的地方。把组件的所有的state更新逻辑提取到组件外的单个函数中,这个函数称为“reducer”。
reducer是处理state的另一种方式。通过三个步骤从useState迁移到useReducer:
- 把设置state函数替换为调度action。
- 写一个reducer函数。
- 使用组件中的reducer。
第一步:把设置state函数替换为调度action
事件处理函数设置state指定React要做什么:
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));
}
删除所有设置state的逻辑后,剩下的是三个事件处理函数:
- 当用户按下“添加”按钮时调用
handleadtask (text)。 - 当用户编辑任务按下“保存”按钮时调用
handleChangeTask(task)。 - 当用户按下“删除”按钮时调用
handleDeleteTask(taskId)。
用reducer管理state与直接设置state有一些不同。不是通过设置state来告诉React“做什么”,而是通过从事件处理程序调度“action”来指定“用户刚刚做了什么”。因此,不是通过事件处理程序“设置任务”,而是调度“添加/更改/删除任务”动作。这更能描述用户的意图。
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的对象称为“action”:
dispatch(
// "action" object:
{
type: 'deleted',
id: taskId,
}
);
它是一个普通的JavaScript对象。你决定在里面放什么,但一般来说,它应该是发生的事情的最小信息。
第二步:写一个reducer函数
一个reducer函数是存放state逻辑的地方。它接受两个参数,当前state和动作对象,并返回下一个state:
function yourReducer(state, action) {
// return next state for React to set
}
React将state的值设置为从reducer返回的state值。
把state逻辑提取到一个reducer函数,reducer函数需要:
- 声明当前的state为第一个参数。
- 声明
action为第二个参数。 - 返回一个state,React把state的值设置为这个返回的state值。
下面是把所有的state逻辑整合的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('Unknown action: ' + action.type);
}
}
reducer函数接受state作为参数,所以可以在组件外部声明reducer函数。这减少了缩进级别,可以使代码更容易阅读。
第三步:使用组件中的reducer
最后,需要将tasksReducer连接到组件。从React中导入useReducer钩子:
import { useReducer } from 'react';
然后替换useState:
const [tasks, setTasks] = useState(initialTasks);
使用useReducer:
const [tasks, dispatch] = useReducer(tasksReducer, initialTasks);
useReducer Hook类似于usestate——传递给它一个初始状态,它返回一个有状态的值和设置状态的方法(有点不同的是在useReduce中返回的是调度函数)。
useReducer钩子接受两个参数:
- 一个reducer函数
- 一个初始state值
返回:
- 一个有状态的值
- 一个调度函数,调度reducer的actions
现在它已经完全连接好了!这里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>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移到另一个文件:
App.js
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},
];
taskReducer.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('Unknown action: ' + action.type);
}
}
}
像这样分离关注点时,组件逻辑可以更容易阅读。现在,事件处理程序只通过分派动作来指定发生了什么,而reducer函数决定状态如何更新以响应它们。
比较useState和useReducer
reducer并非没有缺点!这里有一些方法比较它们:
-
代码量的大小。 一般情况下useState比useReducer的代码量小。使用useReducer,你需要额外写reducer函数和调度的actions。但是,如果许多事件处理程序以相似的方式修改state,useReducer可以帮助减少代码量。
-
可读性。 当状态更新很简单时,使用useState代码非常容易阅读。当状态更新变得更复杂时,它们会使组件的代码膨胀,使其难以阅读。在这种情况下,useReducer可以清晰地将更新逻辑与事件处理分开。
-
调试。 当useState出现错误时,很难判断在哪个地方设置状态出现了错误,以及什么行为导致了错误。使用useReducer,可以在reducer中添加一个控制台日志,能够看到每个state的更新以及是什么行为触发的(根据是哪个action)。如果行为和触发action一致,那么就会知道错误出在reducer逻辑本身。但是,与使用useState相比,useReducer调试时需要遍历更多的代码。
-
测试。 reducer是一个不依赖于组件的纯函数。可以单独导出和测试它。虽然通常最好在更现实的环境中测试组件,但对于复杂的状态更新逻辑,根据初始state值和action,断言reducer返回的state,可能会很有用。
-
个人喜好。 有些人喜欢reducer,有些人不喜欢。没关系。这是个人喜好的问题。总是可以在useState和useReducer之间来回转换:它们是等价的!
写好reducer
在编写reducer时,请记住以下两点:
-
reducers是纯函数。 与更新state函数类似,reducer在渲染过程中运行!(action排队等待直到下一次渲染。)这也意味着reducers必须是纯函数。在reducers函数里面不能发送网络请求,应用timeouts,或是执行其他的任何副作用(影响组件之外的操作)。把objects和array看作不改变数据,不能直接修改。
-
每个action描述了用户的一个交互,即使可能改变会多个数据。 例如一个表单中有5个字段,“reset”按钮用于重新设置这5个字段的值。分派一个reset_form action比分派五个单独的set_field action更有意义。在reducer定义了每一个动作,就很清晰能看到有什么交互,对每一个交互做了什么响应以及交互以什么顺序进行的。
使用Immer简化逻辑
就像更新对象和数组一样,Immer库使reducer更简洁。使用useImmerReducer允许你使用push或arr[i] = assignment来改变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('Unknown 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>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},
];
reducers必须是纯函数,不能直接改变state。但是Immer提供了一个特殊的草稿对象,可以安全地对草稿对象修改。在底层,Immer依据草稿对象的改变创建一个state副本。这就是为什么由useImmerReducer管理的reducer可以改变它们的第一个参数,而不需要返回state。