813 字
4 分钟
useReducer

状态改法散在各处时#

一个任务列表,添加、勾选、删除分别由三个事件处理函数改:

function handleAddTask(text) {
setTasks([...tasks, { id: nextId++, text, done: false }])
}
function handleChangeTask(task) {
setTasks(tasks.map(t => (t.id === task.id ? task : t)))
}
function handleDeleteTask(id) {
setTasks(tasks.filter(t => t.id !== id))
}

三个函数里都藏着「状态长什么样、怎么改」。再加两种改法,规矩就更难统一。

换成 reducer#

const [tasks, dispatch] = useReducer(tasksReducer, initialTasks)

事件处理函数只剩一句「发生了什么」:

dispatch({ type: 'added', id: nextId++, text })

改法全部搬进 tasksReducer:

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 => (t.id === action.task.id ? action.task : t))
case 'deleted':
return tasks.filter(t => t.id !== action.id)
default:
throw Error('Unknown action: ' + action.type)
}
}

签名固定是 (state, action) => nextState。action 的形状随你定,惯例是带 type 字段的对象,其余字段放 reducer 算出新状态所需的最少信息。

末尾那句 throw 是官方文档的写法。漏写 return、或 action 名拼错时,它能把问题直接指出来,好过出现 undefined 再回头找。

dispatch 不等于改值#

dispatch 只请求下一次渲染用新状态。当前这次运行里,state 变量不会变:

function handleClick() {
console.log(tasks.length) // 3
dispatch({ type: 'added', id: nextId++, text: '新任务' })
console.log(tasks.length) // 还是 3
}

这与 useState 是同一套快照模型(State as a Snapshot)。要看新值,等下一次渲染。

另外,reducer 返回的状态与旧状态用 Object.is 比较相等时,React 会跳过重渲染。直接改原对象再返回,就踩到这个坑:

// 不会重渲染
case 'incremented_age': {
state.age++
return state
}
// 返回新对象
case 'incremented_age': {
return { ...state, age: state.age + 1 }
}

...state 别漏。漏了只会剩下你写出来的那几个字段,其余字段会变成 undefined。

初始值要算的时候#

第二个参数是初始值,第三个是可选的初始化函数:

const [state, dispatch] = useReducer(reducer, username, createInitialState)

注意传的是函数本身 createInitialState,不是调用结果 createInitialState(username)。传调用结果的话,每次渲染都会白算一遍。初始化函数不需要入参时,第二个参数传 null。

与 useState 怎么选#

官方给的判据是:多个事件处理函数用不同方式改同一份状态时,用 reducer。此时改法收在一处,事件处理函数只管描述用户做了什么。

反过来,状态之间彼此独立、更新路径也只有一两条,useState 更短,没必要上 reducer。判据全文见 Extracting State Logic into a Reducer。

几条边界#

reducer 必须是纯函数:不请求数据、不起定时器、不碰 DOM。要发请求就在事件处理函数里发,拿到结果再 dispatch。

严格模式下 React 会把 reducer 与初始化函数各调两次,用来暴露不纯的写法。这是开发期行为,生产环境不会。事件处理函数不需要纯,React 也不会把它调两次。

dispatch 的引用在重渲染之间不变,可以放心传给子组件,或放进 Effect 的依赖数组。lint 允许省略时,省略也是安全的。

不可变更新写多了会啰嗦。官方在 useReducer 参考页里给出了用 Immer 简化的版本,细节见本系列的 useImmer 一篇。

第 12 篇,共 39 篇本文属于系列

useReducer
https://www.choria.top/posts/hook-use-reducer/
作者
乔瑞雅·克林威尔
发布于
2026-05-02
许可协议
CC BY-NC-SA 4.0

分享文章

生成精美分享图或复制链接,与更多人分享本文。

继续阅读

沿着主题读

基于共同的标签与分类

换条路线

从其他文章中稳定抽取

最后更新于 ,距今已过 190 天

部分内容可能已过时