React 学习站
React›进阶›进阶

useReducer 与复杂状态

进阶进阶

一句话定义

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 是复杂度溢价,不是身份象征。

自测题

  1. 列出「从 useState 迁移到 useReducer」的三个信号。
  2. 为什么 reducer 必须纯函数?结合 kp-008 的「渲染可重放」说明。
  3. 为什么 dispatch 的引用是稳定的?这对 kp-018 的 memo 意味着什么?
  4. 把购物车「批量加购」设计成 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>