useReducer 实战:从入门到真香

说实话,我刚接触 React 那会儿,觉得 useState 就是万能药。直到有一次,我接手了一个表单页面——十几个字段,还有联动校验、动态增删行……用 useState 写了三百行,自己都看吐了。后来改用 useReducer,代码量砍了一半,逻辑还清晰了。

今天我们就来聊聊 useReducer。它不是什么高深概念,说白了就是一个「加强版的 useState」。你想想看,当状态逻辑变得复杂时,我们需要一个更结构化的管理方式。

一、useReducer 的基本模式

先看最基础的用法。我习惯把 reducer 想象成一个「状态加工厂」:你给它当前状态和一个动作,它返回一个新状态。

import { useReducer } from 'react';

// 1. 定义 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;
  }
}

// 2. 在组件中使用
function Counter() {
  const [state, dispatch] = useReducer(counterReducer, { count: 0 });

  return (
    <div>
      <p>当前计数:{state.count}</p>
      <button onClick={() => dispatch({ type: 'INCREMENT' })}>+1</button>
      <button onClick={() => dispatch({ type: 'DECREMENT' })}>-1</button>
      <button onClick={() => dispatch({ type: 'RESET' })}>重置</button>
    </div>
  );
}

核心要点:

  • reducer 必须是纯函数——同样的输入永远得到同样的输出
  • action 通常是一个对象,至少包含 type 字段
  • dispatch 是触发状态更新的唯一方式

嗯,这里要注意:reducer 里不要做副作用操作(比如发请求、改 DOM)。这些事应该交给 useEffect 或者事件处理函数去做。

二、useReducer vs useState:什么时候该用谁?

我在项目中遇到过不少同事纠结这个问题。其实判断标准很简单:

场景 推荐方案 原因
单个独立的值(如开关、输入框内容) useState 简单直接,代码量少
多个关联状态(如表单字段、购物车) useReducer 状态之间互相依赖,更新逻辑集中
状态更新逻辑复杂(多层嵌套、条件分支) useReducer 把逻辑抽离到 reducer 中,组件更干净
状态需要跨组件传递 useReducer + Context 比 useState 更容易管理复杂的状态树

举个例子。如果你只是控制一个弹窗的显示隐藏,useState 三行搞定:

const [isOpen, setIsOpen] = useState(false);
// 使用:setIsOpen(true)

但如果你要管理一个购物车——添加商品、删除商品、修改数量、计算总价……用 useState 写出来,每个操作都要手动处理状态合并,很容易出 bug。

我的个人习惯:当状态更新逻辑超过 3 个不同的操作类型时,我就会考虑换成 useReducer。这不是硬性规定,但能帮你避免后期重构的痛苦。

三、处理复杂状态逻辑:一个真实案例

我曾经做过一个用户管理面板,需要支持:

  • 加载用户列表
  • 搜索/筛选
  • 分页
  • 选中/取消选中
  • 批量删除

如果用 useState,大概会写成这样:

const [users, setUsers] = useState([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const [page, setPage] = useState(1);
const [selectedIds, setSelectedIds] = useState(new Set());
const [searchTerm, setSearchTerm] = useState('');

然后每个操作都要手动更新多个 state。比如加载成功时:

setLoading(false);
setUsers(data.users);
setError(null);

你想想看,如果漏掉一个 setError(null),之前报错的信息就会一直留在界面上。这种 bug 很难排查。

useReducer 重构后,所有逻辑都集中到 reducer 里:

const initialState = {
  users: [],
  loading: false,
  error: null,
  page: 1,
  selectedIds: new Set(),
  searchTerm: '',
};

function userReducer(state, action) {
  switch (action.type) {
    case 'FETCH_START':
      return { ...state, loading: true, error: null };
    case 'FETCH_SUCCESS':
      return { ...state, loading: false, users: action.payload };
    case 'FETCH_ERROR':
      return { ...state, loading: false, error: action.payload };
    case 'SET_PAGE':
      return { ...state, page: action.payload };
    case 'TOGGLE_SELECT':
      const newSelected = new Set(state.selectedIds);
      if (newSelected.has(action.payload)) {
        newSelected.delete(action.payload);
      } else {
        newSelected.add(action.payload);
      }
      return { ...state, selectedIds: newSelected };
    case 'SET_SEARCH':
      return { ...state, searchTerm: action.payload, page: 1 };
    default:
      return state;
  }
}

避坑指南:我曾经在 reducer 里直接修改了 state.selectedIds,结果界面死活不更新。记住:reducer 必须返回一个新对象,不能直接修改 state。像 new Set(state.selectedIds) 这样创建副本是安全的做法。

组件里调用就清爽多了:

const [state, dispatch] = useReducer(userReducer, initialState);

// 加载用户
dispatch({ type: 'FETCH_START' });
try {
  const data = await fetchUsers(state.page, state.searchTerm);
  dispatch({ type: 'FETCH_SUCCESS', payload: data });
} catch (err) {
  dispatch({ type: 'FETCH_ERROR', payload: err.message });
}

// 切换选中
dispatch({ type: 'TOGGLE_SELECT', payload: userId });

你看,每个操作对应一个 action,逻辑一目了然。而且如果以后要加「全选」「取消全选」功能,只需要在 reducer 里加两个 case 就行,组件代码几乎不用改。

四、进阶技巧:action 携带更多信息

action 不一定只有 type。我经常在 action 里放 payload、meta 等字段:

dispatch({
  type: 'UPDATE_USER',
  payload: { id: 1, name: '张三' },
  meta: { timestamp: Date.now() }
});

这样在 reducer 里可以拿到完整的信息来做更精细的控制。比如根据时间戳决定是否忽略过期请求。

总结一下:

  • useReducer 不是 useState 的替代品,而是互补品
  • 当状态逻辑复杂到让你头疼时,就是 useReducer 出场的时候
  • reducer 让状态更新变得可预测、可测试、可维护

最后说一句:别为了用而用。如果你的组件就两三个状态,用 useState 完全没问题。但一旦发现状态之间「牵一发而动全身」,或者更新逻辑里充满了 if-else 和临时变量,嗯,是时候考虑 useReducer 了。