React: 复杂状态:useReducer

最后更新:2026-10-26

1. 你将学到



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)
100%
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 回放)失效。

📖 小节


📝 作业

  1. 基础题(难度⭐):创建一个 Counter 组件,用 useReducer 实现 +1、-1、+10、重置四个操作。在页面上显示当前值和操作历史(最近 5 次)。
  2. 进阶题(难度⭐⭐):创建一个 TodoApp 组件,用 useReducer + Context 实现添加、切换完成、编辑、删除、清除已完成五个功能。
  3. 挑战题(难度⭐⭐⭐):创建一个 UndoRedoList 组件,用 useReducer 实现"撤销/重做"功能。每个操作保存历史快照,支持 Ctrl+Z 撤销、Ctrl+Shift+Z 重做。
Web-Tutorial.com

Web-Tutorial 技术团队

由多位开发者共同维护的编程教程平台。每篇教程由对应领域的开发者编写和审核,确保内容准确可靠。如发现任何问题,欢迎向我们反馈。

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏