React 中列表操作 reducer 替代多个 useState

FreeGuideOnline 最新 2026-07-06

为什么列表操作不应该使用多个 useState

在 React 中管理列表时,初学者常常会为加载状态、数据、错误、筛选条件等创建多个 useState

const [items, setItems] = useState([]);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState(null);
const [filter, setFilter] = useState('all');

这种模式在逻辑简单时看似清晰,但当操作变得复杂(如批量删除、乐观更新、重试机制)时,你会面临严重问题:

  • 状态一致性难以保证:一次操作需要同时更新多个状态,忘记重置某个状态会导致界面异常。
  • 逻辑分散:状态更新逻辑散落在事件处理函数中,难以复用和测试。
  • 性能风险:多个独立的 setState 调用可能触发额外的渲染。

useReducer 正是解决这类问题的利器。它将所有相关状态合并到一个对象中,并通过 dispatch 派发动作来管理状态转换。


useReducer 如何替代多个 useState

useReducer 接收一个 reducer 函数和初始状态,返回当前状态和 dispatch 方法。

const [state, dispatch] = useReducer(reducer, initialState);
  • state:包含列表数据及其所有元状态(加载、错误等)的单一对象。
  • dispatch:用来发送动作对象,由 reducer 根据动作类型返回新状态。

对比传统 useState 模式

方面 多个 useState useReducer
状态结构 多个独立变量 一个聚合对象
更新逻辑 分散在事件处理器中 集中在 reducer 函数
复杂操作一致性 手动协调多个 setState 原子化更新,不会遗漏
可预测性 易产生中间不一致状态 每个动作都映射到确定的新状态
测试 难以隔离测试业务逻辑 reducer 是纯函数,极易测试
适用场景 简单独立状态 复杂交互的列表、表单、数据获取

实战:用 useReducer 重写列表管理

假设我们要实现一个任务列表,支持获取数据、添加、删除、编辑、切换完成状态、过滤和错误处理

1. 定义状态结构与动作类型

const initialState = {
  data: [],
  isLoading: false,
  error: null,
  filter: 'all',
};

// 动作类型常量(推荐使用常量避免拼写错误)
const ACTIONS = {
  FETCH_START: 'FETCH_START',
  FETCH_SUCCESS: 'FETCH_SUCCESS',
  FETCH_ERROR: 'FETCH_ERROR',
  ADD_TASK: 'ADD_TASK',
  DELETE_TASK: 'DELETE_TASK',
  TOGGLE_TASK: 'TOGGLE_TASK',
  SET_FILTER: 'SET_FILTER',
};

2. 实现 reducer 函数

reducer 必须是一个纯函数:不修改原状态,根据动作返回新状态

function taskReducer(state, action) {
  switch (action.type) {
    case ACTIONS.FETCH_START:
      return { ...state, isLoading: true, error: null };

    case ACTIONS.FETCH_SUCCESS:
      return { ...state, isLoading: false, data: action.payload, error: null };

    case ACTIONS.FETCH_ERROR:
      return { ...state, isLoading: false, error: action.payload };

    case ACTIONS.ADD_TASK:
      return { ...state, data: [...state.data, action.payload] };

    case ACTIONS.DELETE_TASK:
      return {
        ...state,
        data: state.data.filter((task) => task.id !== action.payload),
      };

    case ACTIONS.TOGGLE_TASK:
      return {
        ...state,
        data: state.data.map((task) =>
          task.id === action.payload
            ? { ...task, completed: !task.completed }
            : task
        ),
      };

    case ACTIONS.SET_FILTER:
      return { ...state, filter: action.payload };

    default:
      return state;
  }
}

3. 在组件中使用

import { useReducer, useEffect } from 'react';

function TaskList() {
  const [state, dispatch] = useReducer(taskReducer, initialState);

  const fetchTasks = async () => {
    dispatch({ type: ACTIONS.FETCH_START });
    try {
      const response = await fetch('/api/tasks');
      if (!response.ok) throw new Error('网络请求失败');
      const data = await response.json();
      dispatch({ type: ACTIONS.FETCH_SUCCESS, payload: data });
    } catch (err) {
      dispatch({ type: ACTIONS.FETCH_ERROR, payload: err.message });
    }
  };

  useEffect(() => {
    fetchTasks();
  }, []);

  // 根据 filter 过滤显示数据
  const filteredTasks = state.data.filter((task) => {
    if (state.filter === 'completed') return task.completed;
    if (state.filter === 'active') return !task.completed;
    return true;
  });

  return (
    <div>
      <div>
        <button onClick={() => dispatch({ type: ACTIONS.SET_FILTER, payload: 'all' })}>全部</button>
        <button onClick={() => dispatch({ type: ACTIONS.SET_FILTER, payload: 'active' })}>未完成</button>
        <button onClick={() => dispatch({ type: ACTIONS.SET_FILTER, payload: 'completed' })}>已完成</button>
      </div>

      {state.isLoading && <p>加载中...</p>}
      {state.error && (
        <p>
          错误: {state.error}
          <button onClick={fetchTasks}>重试</button>
        </p>
      )}

      <ul>
        {filteredTasks.map((task) => (
          <li key={task.id}>
            <span style={{ textDecoration: task.completed ? 'line-through' : 'none' }}>
              {task.text}
            </span>
            <button onClick={() => dispatch({ type: ACTIONS.TOGGLE_TASK, payload: task.id })}>
              {task.completed ? '未完成' : '完成'}
            </button>
            <button onClick={() => dispatch({ type: ACTIONS.DELETE_TASK, payload: task.id })}>
              删除
            </button>
          </li>
        ))}
      </ul>

      <input
        type="text"
        onKeyDown={(e) => {
          if (e.key === 'Enter' && e.target.value.trim()) {
            dispatch({
              type: ACTIONS.ADD_TASK,
              payload: { id: Date.now(), text: e.target.value, completed: false },
            });
            e.target.value = '';
          }
        }}
        placeholder="添加新任务"
      />
    </div>
  );
}

进阶技巧:用 useReducer 实现乐观更新

在删除任务时,可以先立即从 UI 中移除该项,再发送 API 请求。如果请求失败,则撤销删除并显示错误。

修改 reducer,添加撤销相关动作:

case ACTIONS.OPTIMISTIC_DELETE:
  return {
    ...state,
    data: state.data.filter((task) => task.id !== action.payload.id),
    backup: state.data, // 备份旧数据
  };

case ACTIONS.ROLLBACK_DELETE:
  return {
    ...state,
    data: action.payload,
    error: '删除失败,已恢复',
  };

组件中的删除处理:

const handleDelete = async (id) => {
  dispatch({ type: ACTIONS.OPTIMISTIC_DELETE, payload: { id } });
  try {
    await fetch(`/api/tasks/${id}`, { method: 'DELETE' });
    // 成功则无需额外处理
  } catch {
    // 失败时恢复数据(需要传入之前备份的数据)
    dispatch({ type: ACTIONS.ROLLBACK_DELETE, payload: state.backup });
  }
};

何时坚持使用 useState

useReducer 虽然强大,但并非所有场景都适用。以下情况用 useState 更合适:

  • 状态之间完全独立,没有联动逻辑。
  • 状态数量极少(如只有一个布尔开关)。
  • 更新逻辑非常直接,不需要维护一致性。

经验法则:如果发现自己在写 setA(…) 的同时还要 setB(…),或者多个状态总是一起变化,那就是切换到 useReducer 的信号。

总结

  • 多个 useState 容易导致状态不一致和代码碎片化,尤其适合列表管理场景。
  • useReducer 将状态聚合、动作分发和纯函数更新结合,使复杂交互变得可预测。
  • 初始状态、reducer 函数和组件派发形成了清晰的三层结构:状态定义 → 状态转换 → 视图触发
  • 通过定义常量化动作,可获得更好的代码补全和拼写错误防护。
  • 对于网络请求的乐观更新、重试等高级模式,reducer 能轻松保持数据完整性。

把列表操作的状态管理交给 useReducer,你的 React 代码会变得更具可维护性和可测试性。