useReducer 是一个 React Hook,可让你向组件添加一个 reducer。
const [state, dispatch] = useReducer(reducer, initialArg, init?)参考
useReducer(reducer, initialArg, init?)
在组件顶层调用 useReducer,通过 reducer 来管理其状态。
import { useReducer } from 'react';
function reducer(state, action) {
// ...
}
function MyComponent() {
const [state, dispatch] = useReducer(reducer, { age: 42 });
// ...参数
reducer:指定状态如何更新的 reducer 函数。它必须是纯函数,应将 state 和 action 作为参数,并返回下一个 state。state 和 action 可以是任意类型。initialArg:用于计算初始 state 的值。它可以是任意类型。如何从它计算初始 state 取决于下一个init参数。- 可选
init:应返回初始 state 的初始化函数。如果未指定,则初始 state 设为initialArg。否则,初始 state 设为调用init(initialArg)的结果。
返回值
useReducer 返回一个恰好包含两个值的数组:
- 当前 state。在第一次渲染时,它被设为
init(initialArg)或initialArg(如果没有init)。 dispatch函数,用于将 state 更新为不同的值并触发重新渲染。
注意事项
useReducer是一个 Hook,因此你只能在组件顶层或你自己的 Hooks 中调用它。你不能在循环或条件语句中调用它。如果你需要那样做,请提取一个新组件并将 state 移入其中。dispatch函数具有稳定的身份,因此你经常会看到它被从 Effect 依赖项中省略,但把它包含进去也不会导致 Effect 触发。如果 lint 工具允许你在没有报错的情况下省略某个依赖项,那就是安全的。 了解更多关于移除 Effect 依赖项的信息。- 在严格模式下,React 会调用你的 reducer 和 initializer 两次,以便 帮助你发现意外的副作用。 这是仅限开发环境的行为,不会影响生产环境。如果你的 reducer 和 initializer 是纯的(它们应该如此),这不会影响你的逻辑。两次调用中的一次结果会被忽略。
dispatch 函数
useReducer 返回的 dispatch 函数可让你将 state 更新为不同的值并触发重新渲染。你需要将 action 作为 dispatch 函数的唯一参数传入:
const [state, dispatch] = useReducer(reducer, { age: 42 });
function handleClick() {
dispatch({ type: 'incremented_age' });
// ...React 会将你提供的 reducer 函数以及当前 state 和你传给 dispatch 的 action 一起调用,并将下一次 state 设置为其结果。
参数
action:用户执行的动作。它可以是任意类型的值。按惯例,action 通常是一个对象,包含一个用于标识它的type属性,以及可选的其他附加信息属性。
返回值
dispatch 函数没有返回值。
注意事项
-
dispatch函数只会更新下一次渲染的 state 变量。如果你在调用dispatch函数后读取 state 变量, 你仍然会得到调用前屏幕上的旧值。 -
如果你提供的新值与当前
state完全相同,按Object.is比较结果判断,React 将跳过重新渲染组件及其子组件。 这是一个优化。React 仍可能需要在忽略结果之前调用你的组件,但这不应影响你的代码。 -
React 会批处理 state 更新。 它会在所有事件处理函数执行完毕并且调用了它们的
set函数之后更新屏幕。这可以防止在单个事件中发生多次重新渲染。在少数情况下,如果你需要更早地强制 React 更新屏幕,例如为了访问 DOM,你可以使用flushSync。
用法
向组件添加 reducer
在组件顶层调用 useReducer,通过 reducer 管理 state。
import { useReducer } from 'react';
function reducer(state, action) {
// ...
}
function MyComponent() {
const [state, dispatch] = useReducer(reducer, { age: 42 });
// ...useReducer 返回一个恰好包含两个项的数组:
- 这个 state 变量的当前 state,初始时设置为你提供的初始 state。
dispatch函数,用于根据交互来更改它。
要更新屏幕上显示的内容,请使用代表用户所做操作的对象调用dispatch,这类操作称为 action:
function handleClick() {
dispatch({ type: 'incremented_age' });
}React 会将当前 state 和 action 传递给你的reducer 函数。你的 reducer 会计算并返回下一次 state。React 会存储该下一次 state,使用它渲染你的组件,并更新 UI。
import { useReducer } from 'react'; function reducer(state, action) { if (action.type === 'incremented_age') { return { age: state.age + 1 }; } throw Error('Unknown action.'); } export default function Counter() { const [state, dispatch] = useReducer(reducer, { age: 42 }); return ( <> <button onClick={() => { dispatch({ type: 'incremented_age' }) }}> 增加年龄 </button> <p>你好!你现在 {state.age} 岁。</p> </> ); }
useReducer 和 useState 非常相似,但它允许你将 state 更新逻辑从事件处理函数移到组件外部的单个函数中。阅读更多关于在 useState 和 useReducer 之间进行选择。
编写 reducer 函数
reducer 函数的声明如下:
function reducer(state, action) {
// ...
}然后你需要填入用于计算并返回下一次 state 的代码。按惯例,通常会把它写成一个 switch 语句。 对于 switch 中的每个 case,计算并返回某个下一次 state。
function reducer(state, action) {
switch (action.type) {
case 'incremented_age': {
return {
name: state.name,
age: state.age + 1
};
}
case 'changed_name': {
return {
name: action.nextName,
age: state.age
};
}
}
throw Error('Unknown action: ' + action.type);
}action 可以有任意结构。按惯例,通常会传入带有 type 属性的对象来标识 action。它应包含 reducer 计算下一次 state 所需的最少信息。
function Form() {
const [state, dispatch] = useReducer(reducer, { name: 'Taylor', age: 42 });
function handleButtonClick() {
dispatch({ type: 'incremented_age' });
}
function handleInputChange(e) {
dispatch({
type: 'changed_name',
nextName: e.target.value
});
}
// ...action 类型名称仅限于你的组件内部使用。每个 action 描述一次单独的交互,即使这会导致数据发生多处变化。 state 的结构是任意的,但通常它会是一个对象或数组。
阅读将 state 逻辑提取到 reducer 中以了解更多内容。
Example 1 of 3: 表单(对象)
在这个示例中,reducer 管理一个包含两个字段的 state 对象:name 和 age。
import { useReducer } from 'react'; function reducer(state, action) { switch (action.type) { case 'incremented_age': { return { name: state.name, age: state.age + 1 }; } case 'changed_name': { return { name: action.nextName, age: state.age }; } } throw Error('Unknown action: ' + action.type); } const initialState = { name: 'Taylor', age: 42 }; export default function Form() { const [state, dispatch] = useReducer(reducer, initialState); function handleButtonClick() { dispatch({ type: 'incremented_age' }); } function handleInputChange(e) { dispatch({ type: 'changed_name', nextName: e.target.value }); } return ( <> <input value={state.name} onChange={handleInputChange} /> <button onClick={handleButtonClick}> 增加年龄 </button> <p>你好,{state.name}。你现在 {state.age} 岁。</p> </> ); }
避免重新创建初始 state
React 会保存初始 state 一次,并在后续渲染中忽略它。
function createInitialState(username) {
// ...
}
function TodoList({ username }) {
const [state, dispatch] = useReducer(reducer, createInitialState(username));
// ...虽然 createInitialState(username) 的结果只会用于初次渲染,但你仍然会在每次渲染时调用这个函数。如果它要创建大型数组或执行昂贵计算,这可能会造成浪费。
为了解决这个问题,你可以把它作为 初始化函数 作为第三个参数传给 useReducer:
function createInitialState(username) {
// ...
}
function TodoList({ username }) {
const [state, dispatch] = useReducer(reducer, username, createInitialState);
// ...注意,你传入的是 createInitialState,也就是 函数本身,而不是 createInitialState(),后者是调用该函数的结果。这样一来,初始 state 在初始化后就不会再次被创建。
在上面的示例中,createInitialState 接收一个 username 参数。如果你的初始化函数不需要任何信息来计算初始 state,那么你可以将 null 作为第二个参数传给 useReducer。
Example 1 of 2: 传递初始化函数
此示例传递了初始化函数,因此 createInitialState 函数只会在初始化期间运行。它不会在组件重新渲染时运行,例如当你在输入框中输入内容时。
import { useReducer } from 'react'; function createInitialState(username) { const initialTodos = []; for (let i = 0; i < 50; i++) { initialTodos.push({ id: i, text: username + "'s task #" + (i + 1) }); } return { draft: '', todos: initialTodos, }; } function reducer(state, action) { switch (action.type) { case 'changed_draft': { return { draft: action.nextDraft, todos: state.todos, }; }; case 'added_todo': { return { draft: '', todos: [{ id: state.todos.length, text: state.draft }, ...state.todos] } } } throw Error('Unknown action: ' + action.type); } export default function TodoList({ username }) { const [state, dispatch] = useReducer( reducer, username, createInitialState ); return ( <> <input value={state.draft} onChange={e => { dispatch({ type: 'changed_draft', nextDraft: e.target.value }) }} /> <button onClick={() => { dispatch({ type: 'added_todo' }); }}>添加</button> <ul> {state.todos.map(item => ( <li key={item.id}> {item.text} </li> ))} </ul> </> ); }
故障排除
我已经派发了一个 action,但日志里拿到的还是旧的 state 值
调用 dispatch 函数不会改变正在运行的代码中的 state:
function handleClick() {
console.log(state.age); // 42
dispatch({ type: 'incremented_age' }); // 请求使用 43 重新渲染
console.log(state.age); // 仍然是 42!
setTimeout(() => {
console.log(state.age); // 还是 42!
}, 5000);
}这是因为state 的行为像快照。更新 state 会请求使用新的 state 值进行另一次渲染,但不会影响你已经在运行中的事件处理函数里的 state JavaScript 变量。
如果你需要推测下一个 state 值,可以自己调用 reducer 手动计算:
const action = { type: 'incremented_age' };
dispatch(action);
const nextState = reducer(state, action);
console.log(state); // { age: 42 }
console.log(nextState); // { age: 43 }我已经派发了一个 action,但屏幕没有更新
如果下一个 state 等于上一个 state,React 会忽略你的更新,这是通过 Object.is 比较得出的结果。这通常发生在你直接修改 state 中的对象或数组时:
function reducer(state, action) {
switch (action.type) {
case 'incremented_age': {
// 🚩 错误:修改已有对象
state.age++;
return state;
}
case 'changed_name': {
// 🚩 错误:修改已有对象
state.name = action.nextName;
return state;
}
// ...
}
}你修改了已有的 state 对象并把它返回了,所以 React 忽略了这次更新。要修复这个问题,你需要确保自己始终是在更新 state 中的对象和更新 state 中的数组,而不是修改它们:
function reducer(state, action) {
switch (action.type) {
case 'incremented_age': {
// ✅ 正确:创建一个新对象
return {
...state,
age: state.age + 1
};
}
case 'changed_name': {
// ✅ 正确:创建一个新对象
return {
...state,
name: action.nextName
};
}
// ...
}
}在派发后,我的 reducer state 的一部分变成了 undefined
确保每个 case 分支在返回新 state 时都复制了所有已有字段:
function reducer(state, action) {
switch (action.type) {
case 'incremented_age': {
return {
...state, // 别忘了这个!
age: state.age + 1
};
}
// ...如果没有上面的 ...state,返回的下一个 state 只会包含 age 字段,而不会有其他任何内容。
在派发后,我的整个 reducer state 都变成了 undefined
如果你的 state 意外变成了 undefined,很可能是你忘了在某个 case 里 return state,或者你的 action 类型没有匹配到任何 case 语句。要找出原因,可以在 switch 外部抛出一个错误:
function reducer(state, action) {
switch (action.type) {
case 'incremented_age': {
// ...
}
case 'edited_name': {
// ...
}
}
throw Error('Unknown action: ' + action.type);
}你也可以使用像 TypeScript 这样的静态类型检查器来发现这类错误。
我遇到了一个错误:“Too many re-renders”
你可能会看到这样的错误:Too many re-renders. React limits the number of renders to prevent an infinite loop. 通常这意味着你在渲染期间无条件地派发了一个 action,所以你的组件进入了一个循环:渲染、派发(导致渲染)、渲染、派发(导致渲染),如此反复。很多时候,这是因为在指定事件处理函数时写错了:
// 🚩 错误:在渲染期间调用处理函数
return <button onClick={handleClick()}>点击我</button>
// ✅ 正确:传递事件处理函数
return <button onClick={handleClick}>点击我</button>
// ✅ 正确:传递一个内联函数
return <button onClick={(e) => handleClick(e)}>点击我</button>如果你找不到这个错误的原因,可以点击控制台中错误旁边的箭头,查看 JavaScript 调用栈,找到导致错误的具体 dispatch 函数调用。
我的 reducer 或 initializer 函数运行了两次
在 Strict Mode 中,React 会调用你的 reducer 和 initializer 函数两次。这不应该破坏你的代码。
这种仅在开发环境中的行为有助于你保持组件纯粹。React 会使用其中一次调用的结果,并忽略另一次调用的结果。只要你的组件、initializer 和 reducer 函数是纯函数,这就不会影响你的逻辑。不过,如果它们不小心变成了不纯函数,这有助于你发现错误。
例如,这个不纯的 reducer 函数会修改 state 中的一个数组:
function reducer(state, action) {
switch (action.type) {
case 'added_todo': {
// 🚩 错误:修改 state
state.todos.push({ id: nextId++, text: action.text });
return state;
}
// ...
}
}因为 React 会调用你的 reducer 函数两次,你会看到 todo 被添加了两次,这样你就知道这里有错误了。在这个例子中,你可以通过替换数组而不是修改它来修复这个错误:
function reducer(state, action) {
switch (action.type) {
case 'added_todo': {
// ✅ 正确:用新的 state 替换
return {
...state,
todos: [
...state.todos,
{ id: nextId++, text: action.text }
]
};
}
// ...
}
}现在这个 reducer 函数是纯的了,多调用一次也不会改变行为。这就是为什么 React 调用它两次有助于你发现错误。**只有组件、initializer 和 reducer 函数需要是纯的。**事件处理函数不需要是纯的,所以 React 绝不会调用你的事件处理函数两次。
阅读保持组件纯粹以了解更多。