React: 复杂状态:useReducer
最后更新:2026-10-26
1. 你将学到
- useReducer 的基本模式(state + action → newState)
- action 类型的设计原则
- 与 useState 的对比(什么时候用哪个)
- useReducer + Context 组合(轻量 Redux)
2. 一个购物车状态管理的故事
(1) 痛点:多个 useState 交互复杂
Alice 在做一个购物车组件,需要管理多个交互:
JSX
function ShoppingCart() {
const [items, setItems] = useState([])
const [isLoading, setIsLoading] = useState(false)
const [error, setError] = useState(null)
const [coupon, setCoupon] = useState(null)
// 添加商品
function addItem(product) {
const existing = items.find(i => i.id === product.id)
if (existing) {
// ❌ 多个 setState 调用顺序依赖当前值
setItems(prev => prev.map(i =>
i.id === product.id ? { ...i, qty: i.qty + 1 } : i
))
} else {
setItems(prev => [...prev, { ...product, qty: 1 }])
}
}
// 应用优惠券
function applyCoupon(code) {
setIsLoading(true)
fetch('/api/coupon/' + code)
.then(res => res.json())
.then(data => {
if (data.valid) {
setCoupon(data)
setError(null)
} else {
setError('优惠券无效')
}
})
.catch(err => setError(err.message))
.finally(() => setIsLoading(false))
}
// 问题:items、isLoading、error、coupon 四个状态相互关联
// 一个操作(如 applyCoupon)要操作 3 个 setState
// 容易漏掉某个状态的更新,导致状态不一致
}
(2) useReducer 的解法
▶ 展开运行示例(77 行)
收益:所有状态变更集中在一个 reducer 函数中——可预测、可测试、容易调试。每个 action 就是一个"发生了什么"的记录。
3. useReducer 的核心模式
| 概念 | 说明 | 示例 |
|---|---|---|
reducer |
纯函数,(state, action) → newState | function cartReducer(state, action) |
action |
描述"发生了什么"的对象 | { type: 'ADD_ITEM', payload: product } |
dispatch |
发送 action 的函数 | dispatch({ type: 'INCREMENT' }) |
initialState |
初始状态对象 | { items: [], total: 0 } |
init 函数 |
惰性初始化(可选第 3 参数) | useReducer(reducer, initialArg, init) |
graph LR
A[组件] -->|dispatch(action)| B[reducer]
B -->|(state, action) => newState| C[新状态]
C -->|更新| A
D[用户操作] -->|触发| A
style B fill:#1890ff,color:#fff
style C fill:#52c41a,color:#fff
(1) 基本语法
JSX
const [state, dispatch] = useReducer(reducer, initialState)
// reducer 是一个纯函数:
function reducer(state, action) {
switch (action.type) {
case 'INCREMENT':
return { ...state, count: state.count + 1 }
case 'SET_NAME':
return { ...state, name: action.payload }
default:
return state
}
}
输出:
TEXT
并排对比:HTML 语法与 JSX 语法展示等价的标记
(2) Action 设计原则
| 原则 | 说明 | 坏例子 | 好例子 |
|---|---|---|---|
| 类型命名用过去式 | 描述"已经发生了什么" | ADD |
ITEM_ADDED |
| 携带最少数据 | 只带必要数据,其他在 reducer 中计算 | dispatch({ type: 'SET_TOTAL', total: 42 }) |
dispatch({ type: 'ITEM_ADDED', item }),total 在 reducer 中计算 |
| 不要处理副作用 | reducer 必须是纯函数(不调 API、不读写 localStorage) | case 'LOGIN': fetch(...) |
在 useEffect 或事件处理中调 API,完成后 dispatch |
4. useReducer vs useState
| 维度 | useState | useReducer |
|---|---|---|
| 状态量 | 3 个以下 | 3 个以上或状态相互关联 |
| 更新逻辑 | 简单(toggle、+1) | 复杂(多个子值相互影响) |
| 可读性 | 好(一眼看懂) | 好(集中管理) |
| 可测试性 | 需渲染组件测试 | reducer 是纯函数,可直接测试 |
| 适用规模 | 小组件、独立状态 | 中大型组件、复杂表单、购物车 |
▶ 示例:同功能不同实现对比
▶ 试一试输出:
TEXT
Count: 0 | Last Action: none | Operations: 0。+1→INCREMENT,-1→DECREMENT,Reset→initial,Set 100。History 记录所有操作。
5. useReducer + Context:轻量全局状态
当多个组件需要共享同一个 reducer 状态时,用 Context 传递 dispatch:
▶ 展开运行示例(86 行)
6. 完整示例:多步骤表单(Wizard)
▶ 展开运行示例(194 行)
输出:
TEXT
显示:"Reset"。状态:count(setter:setCount)。按钮:setCount(c => c + 1)}>+1、setCount(0)}>Reset、dispatch({ type: 'INCREMENT' })}>+1、dispatch({ type: 'DECREMENT' })}>-1。useReducer 管理复杂状态
预期输出:3 步注册表单(账号 → 个人资料 → 偏好设置),每步有校验,可前进后退,最终提交并显示提交结果。
▶ 示例 2:异步数据加载与 useReducer
▶ 展开运行示例(117 行)
输出:
TEXT
点击 User 1/2/3 → ⏳ Loading... → 👤 卡片(User 1、user1@example.com、Administrator、2026-01-15)。"Trigger error" → ❌ "Invalid User ID"
输出:
TEXT
Undo/Redo 编辑器:输入文本 → Save。Undo 撤销,Redo 恢复。展示过去/未来的状态栈
▶ 示例 3:useReducer 实现 Undo/Redo 功能
▶ 试一试输出:
TEXT
输入 → Save → "Current: hello"。Undo → 回退到上一步。Redo → 恢复。展示:Past: [v1, v2] | Future: [v4]
输出:
TEXT
通过 useReducer 实现的注册表单:username、email、password、role、agree 复选框。提交时校验各字段。Reset 清空全部内容
▶ 示例 4:useReducer 管理表单状态
▶ 试一试输出:
TEXT
通过 useReducer 实现的注册表单(Username、Email、Password、Agree 复选框)。提交无效 → 红色错误提示。Reset → 清空所有字段。
输出:
TEXT
购物车 context:带 "Add" 按钮的商品列表。购物车显示商品 + 总计。"Clear" 清空购物车
▶ 示例 5:useReducer + Context 全局购物车
▶ 展开运行示例(62 行)
输出:
TEXT
商品(Keyboard $79、Mouse $49、Monitor $399)带 "Add" 按钮。购物车:商品 × 数量 + 总计。"Clear" 清空购物车。通过 Context 实现全局状态。
❓ 常见问题
Q 为什么 reducer 必须是纯函数?
A 纯函数意味着:相同输入 → 相同输出;不修改外部变量;不产生副作用(不调 API、不读写文件)。React 依赖 reducer 的可预测性来做性能优化(跳过不必要的渲染)和调试(时间旅行)。在 reducer 里调 API 会导致状态变化不可预测,破坏调试体验。
Q 什么时候应该从 useState 切换到 useReducer?
A 当出现以下 3 种情况之一时:① 一个操作需要更新 3 个以上 useState(如点击按钮同时修改 isLoading、error、data);② 状态逻辑复杂到分散在多个事件处理函数中难以追踪;③ 组件有多个相互依赖的状态子值。切换的时机:当 useState 版本让你的组件超过 100 行时。
Q reducer 里面可以写副作用吗(如 fetch 请求)?
A 绝对不行。reducer 必须是纯函数——同样的输入永远返回同样的输出,不能有 API 调用、DOM 操作、随机数等副作用。副作用应该放在 useEffect 或事件处理函数中。reducer 中写副作用会导致:状态不可预测、调试困难、时间旅行(DevTools 回放)失效。
📖 小节
useReducer(reducer, initialState)返回[state, dispatch]- reducer 是一个纯函数:
(state, action) => newState - action 包含
type(描述发生了什么)和可选的payload(数据) - useState 适合简单独立的状态,useReducer 适合复杂相互关联的状态
useReducer + Context可以替代小型项目中的 Redux
📝 作业
- 基础题(难度⭐):创建一个
Counter组件,用 useReducer 实现 +1、-1、+10、重置四个操作。在页面上显示当前值和操作历史(最近 5 次)。 - 进阶题(难度⭐⭐):创建一个
TodoApp组件,用 useReducer + Context 实现添加、切换完成、编辑、删除、清除已完成五个功能。 - 挑战题(难度⭐⭐⭐):创建一个
UndoRedoList组件,用 useReducer 实现"撤销/重做"功能。每个操作保存历史快照,支持 Ctrl+Z 撤销、Ctrl+Shift+Z 重做。