一句话定义
const [state, dispatch] = useReducer(reducer, initialState) 把「状态怎么变」集中到一个纯函数 reducer(currentState, action) => newState 里,组件只负责「发出意图」(dispatch)——这是把 useState 升级为小型状态机。
为什么重要
当多个 state 字段互相依赖(下一步依赖上一步)、更新逻辑分散在多个回调中时,useState 的散弹式 setter 会让不变式(invariant)无法保证。reducer 把所有转移规则集中、可测、可审计——Redux 全家桶的底层思想就建立在这里。
前置知识
- kp-005 不可变更新。
- 数组 reduce 的思想(无副作用、累计出新值)。
核心概念
- Action:描述「发生了什么」的对象
{ type, payload },是意图不是指令。 - Reducer:纯函数
(state, action) => state,不得有副作用、不得依赖外部可变量。 - Dispatch:发出 action;在渲染快照语义上与 setState 相同(连续 dispatch 也按队列处理)。
- 适用信号:字段数 ≥ 3 且互相影响;同一状态有多个入口修改;需要完整的状态转移记录(撤销/时间旅行)。
- 与 Context 组合:官方「Scaling Up」教程的标准搭配——reducer 管逻辑,Context 管分发(dispatch 引用天然稳定)。
原理 / 机制
购物车 reducer 完整示例:
jsxfunction cartReducer(state, action) {
switch (action.type) {
case 'add': {
const { item, qty = 1 } = action;
const existing = state.items.find(i => i.id === item.id);
const items = existing
? state.items.map(i => (i.id === item.id ? { ...i, qty: i.qty + qty } : i))
: [...state.items, { ...item, qty }];
return { ...state, items };
}
case 'remove':
return { ...state, items: state.items.filter(i => i.id !== action.id) };
case 'clear':
return { ...state, items: [] };
case 'checkout/start':
return { ...state, checkout: { status: 'loading' } };
case 'checkout/done':
return { ...state, items: [], checkout: { status: 'ok', order: action.order } };
default:
throw new Error(`未知 action: ${action.type}`); // fail-fast,别静默返回 state
}
}
function Cart() {
const [cart, dispatch] = useReducer(cartReducer, { items: [], checkout: null });
return (
<>
{cart.items.map(i => (
<div key={i.id}>
{i.name} × {i.qty}
<button onClick={() => dispatch({ type: 'remove', id: i.id })}>删</button>
</div>
))}
<button onClick={() => dispatch({ type: 'checkout/start' })}>结算</button>
</>
);
}
三个设计信号都出现了:字段关联(items 与 checkout)、多入口(列表页加购、详情页加购、购物车页增删)、状态机含义(checkout: null → loading → ok)。
为什么 dispatch 可以安全地到处传(不像普通回调需要 useCallback):React 保证 dispatch 引用稳定,memo 子组件不会被它击穿(与 kp-018 呼应)。
reducer 纯函数纪律的具体含义:
jsx// ❌ 副作用:网络请求、时间、随机数
case 'save':
api.save(state); // 请求发出去了,但 reducer 可能被重放(kp-008/022)
return { ...state, savedAt: Date.now() }; // 两次渲染时间不同 → 非纯
// ✅ 副作用放外面:dispatch 前或在事件处理器/effect 中执行
function onSave() {
const snapshot = structuredClone(cart); // 渲染外读最新值
api.save(snapshot).then(() => dispatch({ type: 'save/done', at: Date.now() }));
}
直观类比
useState 像便签本:想到什么记什么,草稿多了互相打架。useReducer 像银行记账:任何资金变动必须以「传票」(action)进入系统,柜员(reducer)按唯一规则账簿(switch)记账,账目永远自洽。审计、对账(调试、撤销、时间旅行)也因此成为可能——Redux DevTools 的「时间旅行」就是重放 action 序列。
实例 / 案例
向导表单(多步状态机):
jsxfunction wizardReducer(state, action) {
switch (action.type) {
case 'next': return state.step < 3 ? { ...state, step: state.step + 1 } : state;
case 'back': return state.step > 1 ? { ...state, step: state.step - 1 } : state;
case 'set': return { ...state, answers: { ...state.answers, ...action.patch } };
default: return state;
}
}
const [wizard, dispatch] = useReducer(wizardReducer, { step: 1, answers: {} });
「越界移动」这类规则在 reducer 里写一次即全局生效——组件里的 if (step < 3) 检查全部消失。
常见误区
- reducer 里做副作用:见上;StrictMode/并发下 reducer 可能被调用多次,副作用会重复执行。
- action 里塞「指令」而不是「事实」:
{ type: 'setFieldXTo5' }把实现写进意图;正确是{ type: 'resize', size: 5 }。 - 无限细分 action:一个 input 一个 action type;合理粒度是「一种业务事件」,参数交给 payload。
- default 静默返回:拼写错误的 action type 被吞掉,排查半天;throw 让错误立刻暴露。
- 所有状态都上 reducer:两个不相关字段的简单开关,useState 更直白;reducer 是复杂度溢价,不是身份象征。
自测题
- 列出「从 useState 迁移到 useReducer」的三个信号。
- 为什么 reducer 必须纯函数?结合 kp-008 的「渲染可重放」说明。
- 为什么 dispatch 的引用是稳定的?这对 kp-018 的 memo 意味着什么?
- 把购物车「批量加购」设计成 action(支持一次加 N 个不同商品),写出 case 分支。
与其他知识点的关系
- 向前:把 kp-005 的不可变更新制度化。
- 向后:与 kp-012 组合构成官方推荐的「中型全局状态」方案;kp-021 中 Redux/Zustand 的 reducer 心智都源于此;kp-025 的 Actions 在「表单提交」这一特定子问题上给出了新的官方方案。
延伸阅读
- useReducer API:<https://react.dev/reference/react/useReducer>
- Scaling Up with Reducer and Context:<https://react.dev/learn/scaling-up-with-reducer-and-context>
- Extracting State Logic into a Reducer:<https://react.dev/learn/extracting-state-logic-into-a-reducer>