Components with many state updates spread across many event handlers can get overwhelming. For these cases, you can consolidate all the state update logic outside your component in a single function, called a reducer. 对于拥有许多状态更新逻辑的组件来说,过于分散的事件处理程序可能会令人不知所措。对于这种情况,你可以将组件的所有状态更新逻辑整合到一个外部函数中,这个函数叫作 reducer。
你将会学习到
- What a reducer function is
- 什么是 reducer 函数
- How to refactor
useState
touseReducer
- 如何将
useState
重构成useReducer
- When to use a reducer
- 什么时候使用 reducer
- How to write one well
- 如何编写一个好的 reducer
使用 reducer 整合状态逻辑 | Consolidate state logic with a reducer
As your components grow in complexity, it can get harder to see at a glance all the different ways in which a component’s state gets updated. For example, the TaskApp
component below holds an array of tasks
in state and uses three different event handlers to add, remove, and edit tasks:
随着组件复杂度的增加,你将很难一眼看清所有的组件状态更新逻辑。例如,下面的 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}, ];
Each of its event handlers calls setTasks
in order to update the state. As this component grows, so does the amount of state logic sprinkled throughout it. To reduce this complexity and keep all your logic in one easy-to-access place, you can move that state logic into a single function outside your component, called a “reducer”.
这个组件的每个事件处理程序都通过 setTasks
来更新状态。随着这个组件的不断迭代,其状态逻辑也会越来越多。为了降低这种复杂度,并让所有逻辑都可以存放在一个易于理解的地方,你可以将这些状态逻辑移到组件之外的一个称为 reducer 的函数中。
Reducers are a different way to handle state. You can migrate from useState
to useReducer
in three steps:
Reducer 是处理状态的另一种方式。你可以通过三个步骤将 useState
迁移到 useReducer
:
- Move from setting state to dispatching actions.
- 将设置状态的逻辑 修改 成 dispatch 的一个 action;
- Write a reducer function.
- 编写 一个 reducer 函数;
- Use the reducer from your component.
- 在你的组件中 使用 reducer。
第 1 步: 将设置状态的逻辑修改成 dispatch 的一个 action | Step 1: Move from setting state to dispatching actions
Your event handlers currently specify what to do by setting state: 你的事件处理程序目前是通过设置状态来 实现逻辑的:
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));
}
Remove all the state setting logic. What you are left with are three event handlers: 移除所有的状态设置逻辑。只留下三个事件处理函数:
handleAddTask(text)
is called when the user presses “Add”.handleAddTask(text)
在用户点击 “添加” 时被调用。handleChangeTask(task)
is called when the user toggles a task or presses “Save”.handleChangeTask(task)
在用户切换任务或点击 “保存” 时被调用。handleDeleteTask(taskId)
is called when the user presses “Delete”.handleDeleteTask(taskId)
在用户点击 “删除” 时被调用。
Managing state with reducers is slightly different from directly setting state. Instead of telling React “what to do” by setting state, you specify “what the user just did” by dispatching “actions” from your event handlers. (The state update logic will live elsewhere!) So instead of “setting tasks
” via an event handler, you’re dispatching an “added/changed/deleted a task” action. This is more descriptive of the user’s intent.
使用 reducer 管理状态与直接设置状态略有不同。它不是通过设置状态来告诉 React “要做什么”,而是通过事件处理程序 dispatch 一个 “action” 来指明 “用户刚刚做了什么”。(而状态更新逻辑则保存在其他地方!)因此,我们不再通过事件处理器直接 “设置 task
”,而是 dispatch 一个 “添加/修改/删除任务” 的 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,
});
}
The object you pass to dispatch
is called an “action”:
你传递给 dispatch
的对象叫做 “action”:
function handleDeleteTask(taskId) {
dispatch(
// "action" object:
// "action" 对象:
{
type: 'deleted',
id: taskId,
}
);
}
It is a regular JavaScript object. You decide what to put in it, but generally it should contain the minimal information about what happened. (You will add the dispatch
function itself in a later step.)
它是一个普通的 JavaScript 对象。它的结构是由你决定的,但通常来说,它应该至少包含可以表明 发生了什么事情 的信息。(在后面的步骤中,你将会学习如何添加一个 dispatch
函数。)
第 2 步: 编写一个 reducer 函数 | Step 2: Write a reducer function
A reducer function is where you will put your state logic. It takes two arguments, the current state and the action object, and it returns the next state: reducer 函数就是你放置状态逻辑的地方。它接受两个参数,分别为当前 state 和 action 对象,并且返回的是更新后的 state:
function yourReducer(state, action) {
// return next state for React to set
// 给 React 返回更新后的状态
}
React will set the state to what you return from the reducer. React 会将状态设置为你从 reducer 返回的状态。
To move your state setting logic from your event handlers to a reducer function in this example, you will: 在这个例子中,要将状态设置逻辑从事件处理程序移到 reducer 函数中,你需要:
- Declare the current state (
tasks
) as the first argument.
- 声明当前状态(
tasks
)作为第一个参数;
- Declare the
action
object as the second argument.
- 声明
action
对象作为第二个参数;
- Return the next state from the reducer (which React will set the state to).
- 从
reducer
返回 下一个 状态(React 会将旧的状态设置为这个最新的状态)。
Here is all the state setting logic migrated to a reducer function:
下面是所有迁移到 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);
}
}
Because the reducer function takes state (tasks
) as an argument, you can declare it outside of your component. This decreases the indentation level and can make your code easier to read.
由于 reducer
函数接受 state(tasks
)作为参数,因此你可以 在组件之外声明它。这减少了代码的缩进级别,提升了代码的可读性。
深入探讨
Although reducers can “reduce” the amount of code inside your component, they are actually named after the reduce()
operation that you can perform on arrays.
尽管 reducer
可以 “减少” 组件内的代码量,但它实际上是以数组上的 reduce()
方法命名的。
The reduce()
operation lets you take an array and “accumulate” a single value out of many:
reduce()
允许你将数组中的多个值 “累加” 成一个值:
const arr = [1, 2, 3, 4, 5];
const sum = arr.reduce(
(result, number) => result + number
); // 1 + 2 + 3 + 4 + 5
The function you pass to reduce
is known as a “reducer”. It takes the result so far and the current item, then it returns the next result. React reducers are an example of the same idea: they take the state so far and the action, and return the next state. In this way, they accumulate actions over time into state.
你传递给 reduce
的函数被称为 “reducer”。它接受 目前的结果
和 当前的值
,然后返回 下一个结果
。React 中的 reducer
和这个是一样的:它们都接受 目前的状态
和 action
,然后返回 下一个状态
。这样,action 会随着时间推移累积到状态中。
You could even use the reduce()
method with an initialState
and an array of actions
to calculate the final state by passing your reducer function to it:
你甚至可以使用 reduce()
方法以及 initialState
和 actions
数组,通过传递你的 reducer
函数来计算最终的状态:
import tasksReducer from './tasksReducer.js'; let initialState = []; let actions = [ {type: 'added', id: 1, text: '参观卡夫卡博物馆'}, {type: 'added', id: 2, text: '看木偶戏'}, {type: 'deleted', id: 1}, {type: 'added', id: 3, text: '打卡列侬墙'}, ]; let finalState = actions.reduce(tasksReducer, initialState); const output = document.getElementById('output'); output.textContent = JSON.stringify(finalState, null, 2);
You probably won’t need to do this yourself, but this is similar to what React does! 你可能不需要自己做这些,但这与 React 所做的很相似!
第 3 步: 在组件中使用 reducer | Step 3: Use the reducer from your component
Finally, you need to hook up the tasksReducer
to your component. Import the useReducer
Hook from React:
最后,你需要将 tasksReducer
导入到组件中。记得先从 React 中导入 useReducer
Hook:
import { useReducer } from 'react';
Then you can replace useState
:
接下来,你就可以替换掉之前的 useState
:
const [tasks, setTasks] = useState(initialTasks);
with useReducer
like so:
只需要像下面这样使用 useReducer
:
const [tasks, dispatch] = useReducer(tasksReducer, initialTasks);
The useReducer
Hook is similar to useState
—you must pass it an initial state and it returns a stateful value and a way to set state (in this case, the dispatch function). But it’s a little different.
useReducer
和 useState
很相似——你必须给它传递一个初始状态,它会返回一个有状态的值和一个设置该状态的函数(在这个例子中就是 dispatch 函数)。但是,它们两个之间还是有点差异的。
The useReducer
Hook takes two arguments:
useReducer
钩子接受 2 个参数:
- A reducer function
- 一个 reducer 函数
- An initial state
- 一个初始的 state
And it returns: 它返回如下内容:
- A stateful value
- 一个有状态的值
- A dispatch function (to “dispatch” user actions to the reducer)
- 一个 dispatch 函数(用来 “派发” 用户操作给 reducer)
Now it’s fully wired up! Here, the reducer is declared at the bottom of the component file: 现在一切都准备就绪了!我们在这里把 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); } } } let nextId = 3; const initialTasks = [ {id: 0, text: '参观卡夫卡博物馆', done: true}, {id: 1, text: '看木偶戏', done: false}, {id: 2, text: '打卡列侬墙', done: false} ];
If you want, you can even move the reducer to a different file: 如果有需要,你甚至可以把 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}, ];
Component logic can be easier to read when you separate concerns like this. Now the event handlers only specify what happened by dispatching actions, and the reducer function determines how the state updates in response to them.
当像这样分离关注点时,我们可以更容易地理解组件逻辑。现在,事件处理程序只通过派发 action
来指定 发生了什么,而 reducer
函数通过响应 actions
来决定 状态如何更新。
对比 useState
和 useReducer
| Comparing useState
and useReducer
Reducers are not without downsides! Here’s a few ways you can compare them: Reducer 并非没有缺点!以下是比较它们的几种方法:
- Code size: Generally, with
useState
you have to write less code upfront. WithuseReducer
, you have to write both a reducer function and dispatch actions. However,useReducer
can help cut down on the code if many event handlers modify state in a similar way. - 代码体积: 通常,在使用
useState
时,一开始只需要编写少量代码。而useReducer
必须提前编写 reducer 函数和需要调度的 actions。但是,当多个事件处理程序以相似的方式修改 state 时,useReducer
可以减少代码量。 - Readability:
useState
is very easy to read when the state updates are simple. When they get more complex, they can bloat your component’s code and make it difficult to scan. In this case,useReducer
lets you cleanly separate the how of update logic from the what happened of event handlers.
- 可读性: 当状态更新逻辑足够简单时,
useState
的可读性还行。但是,一旦逻辑变得复杂起来,它们会使组件变得臃肿且难以阅读。在这种情况下,useReducer
允许你将状态更新逻辑与事件处理程序分离开来。
- Debugging: When you have a bug with
useState
, it can be difficult to tell where the state was set incorrectly, and why. WithuseReducer
, you can add a console log into your reducer to see every state update, and why it happened (due to whichaction
). If eachaction
is correct, you’ll know that the mistake is in the reducer logic itself. However, you have to step through more code than withuseState
.
- 可调试性: 当使用
useState
出现问题时, 你很难发现具体原因以及为什么。 而使用useReducer
时, 你可以在 reducer 函数中通过打印日志的方式来观察每个状态的更新,以及为什么要更新(来自哪个action
)。 如果所有action
都没问题,你就知道问题出在了 reducer 本身的逻辑中。 然而,与使用useState
相比,你必须单步执行更多的代码。
- Testing: A reducer is a pure function that doesn’t depend on your component. This means that you can export and test it separately in isolation. While generally it’s best to test components in a more realistic environment, for complex state update logic it can be useful to assert that your reducer returns a particular state for a particular initial state and action.
- 可测试性: reducer 是一个不依赖于组件的纯函数。这就意味着你可以单独对它进行测试。一般来说,我们最好是在真实环境中测试组件,但对于复杂的状态更新逻辑,针对特定的初始状态和
action
,断言 reducer 返回的特定状态会很有帮助。
- Personal preference: Some people like reducers, others don’t. That’s okay. It’s a matter of preference. You can always convert between
useState
anduseReducer
back and forth: they are equivalent!
- 个人偏好: 并不是所有人都喜欢用 reducer,没关系,这是个人偏好问题。你可以随时在
useState
和useReducer
之间切换,它们能做的事情是一样的!
We recommend using a reducer if you often encounter bugs due to incorrect state updates in some component, and want to introduce more structure to its code. You don’t have to use reducers for everything: feel free to mix and match! You can even useState
and useReducer
in the same component.
如果你在修改某些组件状态时经常出现问题或者想给组件添加更多逻辑时,我们建议你还是使用 reducer。当然,你也不必整个项目都用 reducer,这是可以自由搭配的。你甚至可以在一个组件中同时使用 useState
和 useReducer
。
编写一个好的 reducer | Writing reducers well
Keep these two tips in mind when writing reducers:
编写 reducer
时最好牢记以下两点:
- Reducers must be pure. Similar to state updater functions, reducers run during rendering! (Actions are queued until the next render.) This means that reducers must be pure—same inputs always result in the same output. They should not send requests, schedule timeouts, or perform any side effects (operations that impact things outside the component). They should update objects and arrays without mutations.
- reducer 必须是纯粹的。 这一点和 状态更新函数 是相似的,
reducer
是在渲染时运行的!(actions 会排队直到下一次渲染)。 这就意味着reducer
必须纯净,即当输入相同时,输出也是相同的。它们不应该包含异步请求、定时器或者任何副作用(对组件外部有影响的操作)。它们应该以不可变值的方式去更新 对象 和 数组。
- Each action describes a single user interaction, even if that leads to multiple changes in the data. For example, if a user presses “Reset” on a form with five fields managed by a reducer, it makes more sense to dispatch one
reset_form
action rather than five separateset_field
actions. If you log every action in a reducer, that log should be clear enough for you to reconstruct what interactions or responses happened in what order. This helps with debugging!
- 每个 action 都描述了一个单一的用户交互,即使它会引发数据的多个变化。 举个例子,如果用户在一个由
reducer
管理的表单(包含五个表单项)中点击了重置按钮
,那么 dispatch 一个reset_form
的 action 比 dispatch 五个单独的set_field
的 action 更加合理。如果你在一个reducer
中打印了所有的action
日志,那么这个日志应该是很清晰的,它能让你以某种步骤复现已发生的交互或响应。这对代码调试很有帮助!
使用 Immer 简化 reducer | Writing concise reducers with Immer
Just like with updating objects and arrays in regular state, you can use the Immer library to make reducers more concise. Here, useImmerReducer
lets you mutate the state with push
or arr[i] =
assignment:
与在平常的 state 中 修改对象 和 数组 一样,你可以使用 Immer
这个库来简化 reducer
。在这里,useImmerReducer
让你可以通过 push
或 arr[i] =
来修改 state :
{ "dependencies": { "immer": "1.7.3", "react": "latest", "react-dom": "latest", "react-scripts": "latest", "use-immer": "0.5.1" }, "scripts": { "start": "react-scripts start", "build": "react-scripts build", "test": "react-scripts test --env=jsdom", "eject": "react-scripts eject" }, "devDependencies": {} }
Reducers must be pure, so they shouldn’t mutate state. But Immer provides you with a special draft
object which is safe to mutate. Under the hood, Immer will create a copy of your state with the changes you made to the draft
. This is why reducers managed by useImmerReducer
can mutate their first argument and don’t need to return state.
Reducer 应该是纯净的,所以它们不应该去修改 state。而 Immer 为你提供了一种特殊的 draft
对象,你可以通过它安全地修改 state。在底层,Immer 会基于当前 state 创建一个副本。这就是通过 useImmerReducer
来管理 reducer 时,可以修改第一个参数,且不需要返回一个新的 state 的原因。
摘要
- To convert from
useState
touseReducer
: - 把
useState
转化为useReducer
:- Dispatch actions from event handlers.
- 通过事件处理函数 dispatch actions;
- Write a reducer function that returns the next state for a given state and action.
- 编写一个 reducer 函数,它接受传入的 state 和一个 action,并返回一个新的 state;
- Replace
useState
withuseReducer
.
- 使用
useReducer
替换useState
;
- Reducers require you to write a bit more code, but they help with debugging and testing.
- Reducer 可能需要你写更多的代码,但是这有利于代码的调试和测试。
- Reducers must be pure.
- Reducer 必须是纯净的。
- Each action describes a single user interaction.
- 每个 action 都描述了一个单一的用户交互。
- Use Immer if you want to write reducers in a mutating style.
- 使用 Immer 来帮助你在 reducer 里直接修改状态。
第 1 个挑战 共 4 个挑战: 通过事件处理函数 dispatch actions | 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.
目前,ContactList.js
和 Chat.js
中的事件处理程序包含 // TODO
注释。这就是输入不起作用,点击按钮也不会改变收件人的原因。
Replace these two // TODO
s 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
.
将这两个 // TODO
替换为 dispatch
相应的 action。如果要查看 action 的结构和类型,请查看 messengerReducer.js
中的 reducer。reducer 已经写好了,你不需要再修改它。你只需要在 ContactList.js
和 Chat.js
中 dispatch 相应的 action 即可。
import { useReducer } from 'react'; import Chat from './Chat.js'; import ContactList from './ContactList.js'; import { initialState, messengerReducer } from './messengerReducer'; export default function Messenger() { const [state, dispatch] = useReducer(messengerReducer, initialState); const message = state.message; const contact = contacts.find((c) => c.id === state.selectedId); return ( <div> <ContactList contacts={contacts} selectedId={state.selectedId} dispatch={dispatch} /> <Chat key={contact.id} message={message} contact={contact} dispatch={dispatch} /> </div> ); } const contacts = [ {id: 0, name: 'Taylor', email: 'taylor@mail.com'}, {id: 1, name: 'Alice', email: 'alice@mail.com'}, {id: 2, name: 'Bob', email: 'bob@mail.com'}, ];