
状态改法散在各处时
一个任务列表,添加、勾选、删除分别由三个事件处理函数改:
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 一篇。
分享文章
生成精美分享图或复制链接,与更多人分享本文。
继续阅读
换条路线
从其他文章中稳定抽取
最后更新于 ,距今已过 190 天
部分内容可能已过时