将状态逻辑提取到 reducer 中
具有许多分布在许多事件处理程序中的状态更新的组件可能会让人不知所措。 对于这些情况,您可以将组件外部的所有状态更新逻辑合并到一个称为 reducer 的函数中。
你将学习
- reducer 函数是什么
- 如何将 useState 重构为 useReducer
- 什么时候使用reducer
- 如何reducer写好
使用 reducer 整合状态逻辑
随着您的组件变得越来越复杂,一目了然地了解组件状态更新的所有不同方式会变得越来越困难。 例如,下面的 TaskApp 组件包含一组处于状态的任务,并使用三个不同的事件处理程序来添加、删除和编辑任务:
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”。
Reducers 是处理状态的另一种方式。 您可以通过三个步骤从 useState 迁移到 useReducer:
- 从设置状态转移到调度操作。
- 写一个reducer函数。
- 在组件中使用reducer。
第 1 步:从设置状态转移到派遣操作
您的事件处理程序当前通过设置状态指定要执行的操作:
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)。
使用 reducer 管理状态与直接设置状态略有不同。 不是通过设置状态告诉 React “做什么”,而是通过从事件处理程序中分派“动作”来指定“用户刚刚做了什么”。 (状态更新逻辑将存在于其他地方!)因此,您不是通过事件处理程序“设置任务”,而是调度“添加/更改/删除任务”操作。 这更能描述用户的意图。
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”:
function handleDeleteTask(taskId) {
dispatch(
// "action" object:
{
type: 'deleted',
id: taskId,
}
);
}
它是一个常规的 JavaScript 对象。 您决定在其中放入什么,但通常它应该包含有关发生的事情的最少信息。 (您将在后面的步骤中添加调度函数本身。)
注意
动作对象可以是任何形状。
按照惯例,通常给它一个字符串类型来描述发生的事情,并在其他字段中传递任何附加信息。 该类型特定于组件,因此在此示例中,“added”或“added_task”都可以。 选择一个能说明发生了什么的名字!
dispatch({ // specific to component type: 'what_happened', // other fields go here });
第 2 步:编写 reducer 函数
reducer 函数是放置状态逻辑的地方。 它有两个参数,当前状态和动作对象,并返回下一个状态:
function yourReducer(state, action) {
// return next state for React to set
}
React 会将状态设置为您从 reducer 返回的内容。
在此示例中,要将状态设置逻辑从事件处理程序移动到 reducer 函数,您将:
- 将当前状态(任务)声明为第一个参数。
- 将动作对象声明为第二个参数。
- 从 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('Unknown action: ' + action.type);
}
}
因为 reducer 函数将状态(任务)作为参数,所以您可以在组件外部声明它。 这会降低缩进级别并使您的代码更易于阅读。
注意
上面的代码使用了 if/else 语句,但在 reducer 中使用 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('Unknown action: ' + action.type); } } }
我们建议将每个 case 块包裹在 { 和 } 大括号中,这样在不同 case 中声明的变量就不会相互冲突。 此外,case通常应以return结束。 如果您忘记return,代码将“失败”到下一个case,这可能会导致错误!
如果您还不习惯 switch 语句,那么使用 if/else 完全没问题。
深度阅读:为什么reducer这么叫?
尽管 reducers 可以“减少”组件内的代码量,但它们实际上是根据可以对数组执行的 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 reducers 是相同想法的一个例子:它们采用到目前为止的状态和动作,并返回下一个状态。 通过这种方式,它们会随着时间的推移将动作累积到状态中。
您甚至可以使用带有 initialState 和一组操作的 reduce() 方法,通过将 reducer 函数传递给它来计算最终状态:
// index.html <pre id="output"></pre>
// 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('Unknown action: ' + action.type); } } }
// index.js 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);
[ { "id": 2, "text": "Watch a puppet show", "done": false }, { "id": 3, "text": "Lennon Wall pic", "done": false } ]
你可能不需要自己做这件事,但这和 React 做的很相似!
第 3 步:在组件中使用reducer
最后,您需要将 tasksReducer 连接到您的组件。 确保从 React 导入 useReducer Hook:
import {useReducer} from 'react';
然后你可以替换useState:
const [tasks, setTasks] = useState(initialTasks);
像这样使用useReducer
const [tasks, dispatch] = useReducer(tasksReducer, initialTasks);
useReducer Hook 类似于 useState——您必须向它传递一个初始状态,它返回一个有状态值和一种设置状态的方法(在本例中为 dispatch 函数)。 但这有点不同。
useReducer Hook 有两个参数:
- reducer函数
- 初始状态
它返回:
- 有状态的值
- 调度函数(将用户操作“调度”到reducer)
现在它已经完全连接起来了! 这里,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 移动到不同的文件.
当您像这样分离关注点时,组件逻辑可以更容易阅读。 现在,事件处理程序仅通过调度操作指定发生了什么,reducer 函数确定状态如何更新以响应它们。
对比 useState和useReducer
reducer并非没有缺点! 您可以通过以下几种方式比较它们:
- 代码大小:一般来说,使用 useState 你必须预先编写更少的代码。 使用 useReducer,您必须同时编写 reducer 函数和调度操作。 但是,如果许多事件处理程序以类似方式修改状态,useReducer 可以帮助减少代码。
- 可读性:当状态更新很简单时,useState 非常容易阅读。 当它们变得更复杂时,它们会使您的组件代码膨胀并使其难以扫描。 在这种情况下,useReducer 可以让您清楚地将更新逻辑的方式与事件处理程序发生的事情分开。
- 调试:当您遇到 useState 的错误时,可能很难判断状态设置错误的位置以及原因。 使用 useReducer,您可以将控制台日志添加到您的 reducer 中,以查看每个状态更新,以及它发生的原因(由于哪个操作)。 如果每个动作都是正确的,你就会知道错误出在 reducer 逻辑本身。 但是,与 useState 相比,您必须逐步执行更多代码。
- 测试:reducer 是一个不依赖于您的组件的纯函数。 这意味着您可以隔离地单独导出和测试它。 虽然通常最好在更真实的环境中测试组件,但对于复杂的状态更新逻辑,断言你的 reducer 为特定的初始状态和操作返回特定的状态可能很有用。
- 个人喜好:有些人喜欢reducer,有些人不喜欢。 没关系。 这是一个偏好问题。 你总是可以在 useState 和 useReducer 之间来回转换:它们是等价的!
如果你经常遇到一些组件由于状态更新不正确而导致的错误,并且想在其代码中引入更多结构,我们建议使用 reducer。 您不必为所有事情都使用 reducer:随意混合搭配! 您甚至可以在同一组件中使用 useState 和 useReducer。
写好reducer
编写 reducer 时请牢记这两个提示:
- 减速器必须是纯净的。 类似于状态更新函数,reducers 在渲染期间运行! (动作在下一次渲染之前排队。)这意味着 reducer 必须是纯的——相同的输入总是产生相同的输出。 他们不应该发送请求、安排超时或执行任何副作用(影响组件外部事物的操作)。 他们应该无突变地更新对象和数组。
- 每个操作都描述了一次用户交互,即使这会导致数据发生多次更改。 例如,如果用户在具有由 reducer 管理的五个字段的表单上按下“重置”,则分派一个 reset_form 操作比分派五个单独的 set_field 操作更有意义。 如果您记录 reducer 中的每个操作,那么该日志应该足够清晰,以便您重建以何种顺序发生的交互或响应。 这有助于调试!
使用Immer编写精确的reducer
就像在常规状态下更新对象和数组一样,您可以使用 Immer 库使 reducer 更加简洁。 在这里,useImmerReducer 允许您使用 push 或 arr[i] = assignment 改变状态:
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 必须是纯的,所以它们不应该改变状态。 但是 Immer 为您提供了一个可以安全变异的特殊草稿对象。 在后台,Immer 将使用您对草稿所做的更改创建您的状态副本。 这就是为什么由 useImmerReducer 管理的 reducer 可以改变它们的第一个参数并且不需要返回状态。
回顾
- 从 useState 转换为 useReducer:
- 从事件处理程序调度操作。
- 编写一个 reducer 函数,返回给定状态和操作的下一个状态。
- 将 useState 替换为 useReducer。
- Reducers 需要您编写更多代码,但它们有助于调试和测试。
- reducer必须是纯净的。
- 每个操作都描述了一次用户交互。
- 如果你想以可变风格编写 reducer,请使用 Immer。
网友评论