React 18 useReducer 进阶:结合 Context 实现全局状态管理的 5 个最佳实践

在构建现代 React 应用时,状态管理一直是开发者面临的核心挑战之一。随着应用规模的扩大,简单的 useState 可能无法满足复杂状态逻辑的需求,而 Redux 又可能显得过于重量级。React 18 提供的 useReducer Context API 的组合,为我们提供了一种轻量级且强大的替代方案。

1. 理解 useReducer 的核心优势

useReducer 是 React 提供的一个 Hook,它允许你将状态更新逻辑从组件中抽离出来,集中到一个 reducer 函数中管理。与 useState 相比, useReducer 更适合处理包含多个子值的复杂状态,或者当下一个状态依赖于之前的状态时。

reducer 函数的基本结构

function reducer(state, action) {
  switch (action.type) {
    case 'ACTION_TYPE_1':
      return { ...state, /* 更新部分状态 */ };
    case 'ACTION_TYPE_2':
      return { ...state, /* 更新部分状态 */ };
    default:
      throw new Error('未知 action 类型');
  }
}

何时选择 useReducer 而非 useState

  • 状态逻辑复杂,包含多个子值
  • 下一个状态依赖于前一个状态
  • 需要更可预测的状态更新方式
  • 需要在组件间共享状态更新逻辑

2. 创建全局状态上下文

要实现全局状态管理,我们需要结合 Context API。首先创建一个上下文来存储状态和 dispatch 函数:

import { createContext } from 'react';

export const TasksStateContext = createContext(null);
export const TasksDispatchContext = createContext(null);

最佳实践 :将状态和 dispatch 分离到不同的上下文中。这样做可以优化性能,因为只有需要更新状态的组件才需要订阅 dispatch 上下文。

3. 构建 TasksProvider 组件

创建一个 Provider 组件来封装状态逻辑:

import { useReducer } from 'react';
import { TasksStateContext, TasksDispatchContext } from './TasksContext';

function TasksProvider({ children }) {
  const [tasks, dispatch] = useReducer(tasksReducer, initialState);

  return (
    <TasksStateContext value={tasks}>
      <TasksDispatchContext value={dispatch}>
        {children}
      </TasksDispatchContext>
    </TasksStateContext>
  );
}

关键点

  • 使用 useReducer 初始化状态和 dispatch 函数
  • 通过 Context.Provider 向下传递值
  • 保持 Provider 的纯粹性,不包含业务逻辑

4. 处理异步操作

reducer 本身是同步的,但我们可以通过以下模式处理异步操作:

async function fetchTasks(dispatch) {
  dispatch({ type: 'FETCH_TASKS_START' });
  try {
    const response = await fetch('/api/tasks');
    const data = await response.json();
    dispatch({ type: 'FETCH_TASKS_SUCCESS', payload: data });
  } catch (error) {
    dispatch({ type: 'FETCH_TASKS_FAILURE', error });
  }
}

// 在组件中使用
function TaskList() {
  const dispatch = useContext(TasksDispatchContext);
  
  useEffect(() => {
    fetchTasks(dispatch);
  }, [dispatch]);
  
  // ...
}

异步 action 处理模式

  1. 在 action creator 中处理异步逻辑
  2. 在异步操作的不同阶段 dispatch 不同的 action
  3. reducer 只负责同步状态更新

5. 性能优化与常见陷阱

使用 useReducer Context 时需要注意以下性能问题:

避免不必要的重渲染

// 错误示例:直接传递整个上下文值
const value = { tasks, dispatch };
<TasksContext.Provider value={value}>
  {children}
</TasksContext.Provider>

// 正确做法:分离状态和 dispatch
<TasksStateContext.Provider value={tasks}>
  <TasksDispatchContext.Provider value={dispatch}>
    {children}
  </TasksDispatchContext.Provider>
</TasksStateContext.Provider>

使用 useMemo 和 useCallback 优化

function TasksProvider({ children }) {
  const [tasks, dispatch] = useReducer(tasksReducer, initialState);
  
  // 使用 useMemo 避免 tasks 引用变化导致的不必要重渲染
  const memoizedTasks = useMemo(() => tasks, [tasks]);
  
  // 使用 useCallback 避免 dispatch 函数变化
  const memoizedDispatch = useCallback(dispatch, []);
  
  return (
    <TasksStateContext.Provider value={memoizedTasks}>
      <TasksDispatchContext.Provider value={memoizedDispatch}>
        {children}
      </TasksDispatchContext.Provider>
    </TasksStateContext.Provider>
  );
}

常见陷阱及解决方案

陷阱 解决方案
大型状态对象导致性能问题 拆分多个 reducer 和 context
不必要的组件重渲染 使用 React.memo 或分离上下文
异步操作难以追踪 使用明确的 action 类型和错误处理
状态更新顺序问题 确保 reducer 是纯函数

6. 类型安全与可维护性

对于 TypeScript 项目,我们可以为状态和 action 添加类型定义:

interface Task {
  id: number;
  text: string;
  done: boolean;
}

type TasksState = Task[];

type TasksAction =
  | { type: 'added'; id: number; text: string }
  | { type: 'changed'; task: Task }
  | { type: 'deleted'; id: number };

function tasksReducer(state: TasksState, action: TasksAction): TasksState {
  // reducer 实现
}

类型安全的好处

  • 在编译时捕获 action 类型错误
  • 自动补全和类型检查提高开发效率
  • 更清晰的代码文档

7. 测试策略

reducer 作为纯函数,非常适合单元测试:

describe('tasksReducer', () => {
  it('处理 added action', () => {
    const initialState = [];
    const action = { type: 'added', id: 1, text: '测试任务' };
    const nextState = tasksReducer(initialState, action);
    expect(nextState).toEqual([
      { id: 1, text: '测试任务', done: false }
    ]);
  });
  
  it('处理 changed action', () => {
    const initialState = [{ id: 1, text: '旧任务', done: false }];
    const action = { 
      type: 'changed', 
      task: { id: 1, text: '新任务', done: true } 
    };
    const nextState = tasksReducer(initialState, action);
    expect(nextState).toEqual([
      { id: 1, text: '新任务', done: true }
    ]);
  });
});

测试金字塔

  1. 单元测试:专注于 reducer 函数本身
  2. 集成测试:验证组件与上下文的交互
  3. E2E 测试:完整用户流程验证

8. 与 Redux 的对比

虽然 useReducer + Context 可以替代 Redux 的许多功能,但它们之间仍有重要区别:

特性 useReducer + Context Redux
学习曲线 较低 中等
中间件支持 有限 丰富
开发工具 基本 强大
性能优化 手动 自动
适用场景 中小型应用 大型复杂应用

何时选择 Redux

  • 需要强大的开发工具支持
  • 项目中有大量异步逻辑
  • 需要中间件生态系统
  • 团队已经熟悉 Redux

在实际项目中,我经常发现 useReducer + Context 的组合已经能够满足大多数中小型应用的需求,而且代码更加简洁。但对于需要复杂状态管理或团队协作的大型项目,Redux 仍然是更可靠的选择。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐