3、React中的useReducer:与useState的对比、Reducer模式、Dispatch机制、复杂状态逻辑

聊到状态管理,很多人第一反应就是 useState。确实,它简单、直接,80% 的场景用它就够了。但剩下的 20%,比如表单 wizard、购物车、多步骤流程——你会发现 useState 开始变得力不从心。这时候,useReducer 就该登场了。

我个人习惯把 useReducer 看作是 useState 的「升级版」。它不是要取代谁,而是帮你把复杂的状态逻辑收拢到一个地方。说白了,就是让代码更「讲道理」。

useState vs useReducer:什么时候该换?

先看一个简单的对比表格,帮你快速建立直觉:

维度 useState useReducer
状态结构 简单值(string、number、boolean) 复杂对象(嵌套、多字段)
更新逻辑 分散在多个 setState 调用中 集中在一个 reducer 函数中
依赖关系 多个状态之间相互影响时难维护 通过 action 类型清晰表达意图
可测试性 需要渲染组件才能测试 纯函数,直接测 reducer 即可
学习曲线 低,几乎零门槛 中等,需要理解 reducer 模式

我在项目中遇到过这样一个场景:一个多步骤的注册表单,每一步都有校验、缓存、回退。用 useState 写了七八个状态变量,最后自己都分不清哪个先更新、哪个后更新。后来改成 useReducer,一个 action 搞定一步,逻辑清晰多了。

核心判断标准: 如果下一个状态需要依赖上一个状态,或者多个状态需要一起更新,优先考虑 useReducer。

Reducer 模式:纯函数的力量

Reducer 本质上就是一个纯函数:(state, action) => newState。它接收当前状态和一个描述「发生了什么」的 action,然后返回新的状态。

为什么强调「纯函数」?因为纯函数没有副作用,同样的输入永远得到同样的输出。这意味着你可以放心地写单元测试,不用担心组件渲染环境的影响。

// 一个典型的 reducer
function counterReducer(state, action) {
  switch (action.type) {
    case 'INCREMENT':
      return { count: state.count + 1 };
    case 'DECREMENT':
      return { count: state.count - 1 };
    case 'RESET':
      return { count: 0 };
    default:
      return state;
  }
}

嗯,这里要注意:永远不要直接修改 state。Reducer 必须返回一个新的对象,而不是在原有对象上改属性。这是 React 做浅比较依赖的基础。

我曾经踩过的坑: 在 reducer 里直接 push 数组元素,结果界面死活不更新。后来才发现,数组引用没变,React 认为状态没变。记住:return [...state.items, newItem] 而不是 state.items.push(newItem)。

Dispatch 机制:谁触发了更新?

useReducer 返回一个 dispatch 函数,你通过它发送 action 给 reducer。这个 dispatch 是稳定的——它的引用在组件生命周期内不会变,所以不需要放在 useEffect 的依赖数组里。

const [state, dispatch] = useReducer(counterReducer, { count: 0 });

// 发送 action
dispatch({ type: 'INCREMENT' });
dispatch({ type: 'DECREMENT' });
dispatch({ type: 'RESET' });

你想想看,dispatch 其实就是一个「事件发射器」。它不关心 reducer 内部怎么处理,只负责把 action 丢过去。这种解耦让代码更容易理解和维护。

我个人习惯把 action 定义成常量,避免拼写错误:

const ACTIONS = {
  INCREMENT: 'INCREMENT',
  DECREMENT: 'DECREMENT',
  RESET: 'RESET',
};

// 使用
dispatch({ type: ACTIONS.INCREMENT });
小技巧: 如果 action 需要携带额外数据,用 payload 字段。比如 dispatch({ type: 'SET_NAME', payload: '张三' })。这样 reducer 里就能通过 action.payload 拿到数据。

复杂状态逻辑实战:购物车

光说不练假把式。我们来看一个真实场景——购物车。它涉及添加、删除、修改数量、计算总价,状态之间相互依赖,非常适合用 useReducer。

const initialState = {
  items: [],
  total: 0,
};

function cartReducer(state, action) {
  switch (action.type) {
    case 'ADD_ITEM': {
      const existing = state.items.find(item => item.id === action.payload.id);
      let newItems;
      if (existing) {
        newItems = state.items.map(item =>
          item.id === action.payload.id
            ? { ...item, quantity: item.quantity + 1 }
            : item
        );
      } else {
        newItems = [...state.items, { ...action.payload, quantity: 1 }];
      }
      return {
        items: newItems,
        total: newItems.reduce((sum, item) => sum + item.price * item.quantity, 0),
      };
    }
    case 'REMOVE_ITEM': {
      const newItems = state.items.filter(item => item.id !== action.payload.id);
      return {
        items: newItems,
        total: newItems.reduce((sum, item) => sum + item.price * item.quantity, 0),
      };
    }
    case 'UPDATE_QUANTITY': {
      const newItems = state.items.map(item =>
        item.id === action.payload.id
          ? { ...item, quantity: action.payload.quantity }
          : item
      );
      return {
        items: newItems,
        total: newItems.reduce((sum, item) => sum + item.price * item.quantity, 0),
      };
    }
    default:
      return state;
  }
}

看到没?所有逻辑都收拢在 reducer 里。组件只需要调用 dispatch({ type: 'ADD_ITEM', payload: product }),剩下的 reducer 帮你搞定。测试也简单:直接给 reducer 传不同的 action,断言返回的状态对不对。

为什么这样写更好? 因为当业务逻辑变复杂时(比如加优惠券、满减、库存校验),你只需要在 reducer 里加新的 case,组件代码几乎不用动。这就是「关注点分离」的魅力。

useReducer 的 SVG 流程图

下面这张图展示了 useReducer 的完整数据流。从组件发起 dispatch,到 reducer 处理,再到状态更新触发重新渲染,一目了然。

useReducer 数据流 React 组件 dispatch(action) Reducer (state, action) => newState 返回 newState 新状态 触发重新渲染 关键点: • dispatch 是稳定的,不会导致不必要的重新渲染 • reducer 必须是纯函数,不能有副作用

什么时候别用 useReducer?

说了这么多好处,也得泼点冷水。不是所有场景都适合 useReducer。

  • 状态很简单:比如一个开关、一个输入框的值,用 useState 就够了。强行上 reducer 反而增加样板代码。
  • 状态更新不频繁:如果只是偶尔改一下,useState 更轻量。
  • 团队不熟悉 reducer 模式:如果团队成员都是新手,引入 reducer 可能带来理解成本。这时候可以先用 useState,等逻辑复杂了再重构。
我的建议: 从 useState 开始,当发现状态逻辑开始「散落一地」时,再迁移到 useReducer。不要一开始就过度设计。

总结一下

useReducer 不是银弹,但它确实是处理复杂状态逻辑的好工具。它让状态更新变得可预测、可测试、可维护。记住三个核心点:

  1. Reducer 是纯函数——不要修改 state,返回新对象。
  2. Dispatch 是稳定的——不用担心引用变化导致的问题。
  3. Action 描述意图——用 type 和 payload 清晰表达「发生了什么」。

下次遇到复杂状态,不妨试试 useReducer。你会发现,代码变得更有条理了。