目 录CONTENT

文章目录

React(十三)useReducer

React 19 中 useReducer 完全教学指南


一、定义模块:先搞清楚 useReducer 是什么

1.1 核心定位:复杂状态逻辑的“中央调度器”

useReducer 是 React 提供的一个状态管理 Hook,它和 useState 一样用来管理组件内部状态,但更适合处理复杂、多状态、状态之间存在关联的场景。

通俗地说:

  • useState 像一个“抽屉”,每个状态独立管理,适合简单值(如开关、数字、字符串)。
  • useReducer 像一个“中央调度器”:所有状态变更都通过统一的 dispatch 入口,交给一个 reducer 函数来决定新状态是什么。

它的核心设计思想是:把“状态更新的逻辑”和“展示状态的 UI”解耦。UI 只负责“发命令”,真正决定怎么更新状态的是 reducer。

生活化类比:餐厅后厨

想象你去一家餐厅点菜:

  • 你(UI)只告诉服务员:“我要点一份宫保鸡丁”。
  • 服务员(dispatch)把订单送到后厨。
  • 后厨的大厨(reducer)根据订单内容,决定怎么配菜、怎么炒、最终给你一盘什么菜(新状态)。

你不需要知道厨房里面怎么操作,只需要会“点菜”(dispatch)。这就是 useReducer 的设计哲学。


1.2 与 useState 的适用差异

场景 useState useReducer
状态是简单值(数字、字符串、布尔值) 非常适合 有点大材小用
多个状态相互关联、需要同时更新 代码会散乱 非常适合
状态更新逻辑复杂、有多种分支 需要写很多函数 非常适合
需要跨组件共享状态 需配合 Context 配合 Context 更优雅
predictable state transitions(可预测的状态流转) 靠开发者自觉 天然结构化

简单结论:能用 useState 搞定的事情就用 useState;当状态变复杂、分支变多、多个状态需要协同变化时,再考虑 useReducer


1.3 完整 API 参数拆解

useReducer 的标准写法如下:

const [state, dispatch] = useReducer(reducer, initState, init);

它接收三个参数,返回一个数组 [state, dispatch]

参数一:reducer(纯函数)

function reducer(state, action) {
  // 根据 action.type 判断要做什么
  // 返回新的状态,不能修改原 state
}
  • 作用:接收当前状态 state 和命令 action,返回新状态。
  • 类型约束(state: S, action: A) => S
  • 关键要求:必须是纯函数——不能修改参数,不能有副作用,相同输入必须返回相同输出。

参数二:initState(初始状态)

const initState = { count: 0 };
  • 作用:定义组件第一次渲染时的初始状态。
  • 类型约束:任意类型,通常是一个对象或数组。

参数三:init(可选的惰性初始化函数)

function init(initialArg) {
  // 对 initialArg 做处理,返回初始状态
  return { count: initialArg };
}
  • 作用:把初始状态值的计算逻辑抽出来,只在组件挂载时执行一次,避免每次渲染重复计算。
  • 类型约束(arg: I) => S
  • 使用场景:初始状态需要复杂计算、从 props 或 localStorage 恢复时非常有用。

返回值:[state, dispatch]

  • state:当前状态值。
  • dispatch:触发状态更新的函数。调用方式:dispatch({ type: 'INCREMENT' })

1.4 React 19 中 useReducer 的底层执行逻辑(简化版)

本节仅保留教学所需的核心逻辑,不过度深入源码。

当你调用 dispatch(action) 时,React 内部会做以下事情:

  1. 把 action 存入更新队列:React 不会立即修改状态,而是先把这次更新请求记录下来。
  2. 触发重新调度:React 19 引入的并发渲染机制会让更新可以被中断、优先级重新排序。
  3. 在合适的时机执行 reducer:React 取出当前状态 state 和队列中的 action,调用 reducer(state, action) 得到新状态。
  4. 比较并渲染:如果新状态与旧状态引用不同,React 会重新渲染组件。
  5. 批处理优化:React 19 会自动把多个 dispatch 合并到一次渲染中,减少不必要的重渲染。

关键点:

  • dispatch 本身不会立刻改变状态,它只是“提交一个更新请求”。
  • 真正决定新状态的是 reducer,所以 reducer 必须纯函数
  • React 19 的自动批处理会让多个 dispatch 尽量合并,提高效率。

1.5 类型定义示例(JS + TS 双版本)

JavaScript 版本

// 初始状态
const initState = {
  count: 0,
  username: "",
};

// reducer 函数
function reducer(state, action) {
  switch (action.type) {
    case "INCREMENT":
      return { ...state, count: state.count + 1 };
    case "SET_USERNAME":
      return { ...state, username: action.payload };
    default:
      return state;
  }
}

// 使用
const [state, dispatch] = useReducer(reducer, initState);

TypeScript 版本

import { useReducer } from "react";

// 1. 定义状态类型
interface State {
  count: number;
  username: string;
}

// 2. 定义 action 类型(使用联合类型精确约束)
type Action =
  | { type: "INCREMENT" }
  | { type: "DECREMENT" }
  | { type: "SET_USERNAME"; payload: string };

// 3. 初始状态
const initState: State = {
  count: 0,
  username: "",
};

// 4. reducer 函数
function reducer(state: State, action: Action): State {
  switch (action.type) {
    case "INCREMENT":
      return { ...state, count: state.count + 1 };
    case "DECREMENT":
      return { ...state, count: state.count - 1 };
    case "SET_USERNAME":
      return { ...state, username: action.payload };
    default:
      // 这里用 exhaustive check 可以在 TS 中保证 action 都被处理
      const _exhaustiveCheck: never = action;
      return _exhaustiveCheck;
  }
}

// 5. 使用
const [state, dispatch] = useReducer(reducer, initState);

高亮提示:TypeScript 的联合类型 type Action = ... 是约束 action 合法取值的关键手段,强烈推荐在团队项目中使用。


定义模块小结

  • useReducer 是 React 的“中央调度器”式状态管理 Hook,适合复杂、多状态、有关联的状态管理。
  • 它比 useState 更结构化,把“更新逻辑”从 UI 中解耦出来。
  • 核心 API:useReducer(reducer, initState, init),返回 [state, dispatch]
  • reducer 必须是纯函数;init 可选,用于惰性初始化。
  • React 19 中 dispatch 会被批量处理和并发调度,不会立即修改状态。

二、使用模块:从 0 到 1 的实战案例

2.1 案例一:Counter 计数器(基础用法)

这是最简单的 useReducer 示例,帮助你建立对 API 的整体印象。

import { useReducer } from "react";

// 步骤 1:定义初始状态
// 初始状态是一个对象,包含计数器的值
const initState = {
  count: 0,
};

// 步骤 2:定义 reducer 纯函数
// 参数 state:当前状态
// 参数 action:一个描述“要做什么”的对象,必须包含 type 字段
function reducer(state, action) {
  switch (action.type) {
    // 当 action.type 为 'increment' 时, count + 1
    case "increment":
      return { count: state.count + 1 };
    // 当 action.type 为 'decrement' 时, count - 1
    case "decrement":
      return { count: state.count - 1 };
    // 当 action.type 为 'reset' 时,回到初始状态
    case "reset":
      return { count: 0 };
    // 默认情况下返回原状态,保持 reducer 安全
    default:
      return state;
  }
}

// 步骤 3:在组件中使用 useReducer
function Counter() {
  // 解构出当前状态 state 和 dispatch 函数
  const [state, dispatch] = useReducer(reducer, initState);

  return (
    <div>
      {/* 读取状态 */}
      <p>当前计数:{state.count}</p>

      {/* 步骤 4:通过 dispatch 触发状态更新 */}
      {/* dispatch 的参数是一个 action 对象 */}
      <button onClick={() => dispatch({ type: "increment" })}>+1</button>
      <button onClick={() => dispatch({ type: "decrement" })}>-1</button>
      <button onClick={() => dispatch({ type: "reset" })}>重置</button>
    </div>
  );
}

export default Counter;

代码逐行解读

代码 作用
const initState = { count: 0 } 定义初始状态
function reducer(state, action) 定义 reducer,接收当前状态和 action
switch (action.type) 根据 action 的 type 字段决定如何更新状态
return { count: state.count + 1 } 返回新状态对象,不修改原 state
const [state, dispatch] = useReducer(reducer, initState) 注册 reducer,获取 state 和 dispatch
dispatch({ type: 'increment' }) 向 reducer 发送“增加”命令

高亮提示dispatch 的参数 { type: 'increment' } 就是 action 对象。type 是 action 的“身份证”,reducer 通过它判断你的意图。


2.2 案例二:Todo 待办列表(复杂多状态处理)

待办列表包含多个状态:列表数据、输入框值、筛选条件等,是练习 useReducer 的经典场景。

import { useReducer } from "react";

// 步骤 1:定义初始状态(包含多个相关联的状态)
const initState = {
  todos: [], // 待办事项列表
  inputValue: "", // 输入框内容
  filter: "all", // 筛选条件:all / active / completed
};

// 步骤 2:定义 reducer
function reducer(state, action) {
  switch (action.type) {
    // 修改输入框内容
    case "SET_INPUT":
      return { ...state, inputValue: action.payload };

    // 新增待办事项
    case "ADD_TODO": {
      // 如果输入框为空,不添加
      if (!state.inputValue.trim()) return state;

      const newTodo = {
        id: Date.now(),
        text: state.inputValue,
        completed: false,
      };

      return {
        ...state,
        todos: [...state.todos, newTodo],
        inputValue: "", // 添加完成后清空输入框
      };
    }

    // 切换待办事项的完成状态
    case "TOGGLE_TODO":
      return {
        ...state,
        todos: state.todos.map((todo) =>
          todo.id === action.payload
            ? { ...todo, completed: !todo.completed }
            : todo,
        ),
      };

    // 删除待办事项
    case "DELETE_TODO":
      return {
        ...state,
        todos: state.todos.filter((todo) => todo.id !== action.payload),
      };

    // 修改筛选条件
    case "SET_FILTER":
      return { ...state, filter: action.payload };

    default:
      return state;
  }
}

function TodoApp() {
  // 步骤 3:使用 useReducer
  const [state, dispatch] = useReducer(reducer, initState);

  // 步骤 4:根据筛选条件计算要展示的数据
  const filteredTodos = state.todos.filter((todo) => {
    if (state.filter === "active") return !todo.completed;
    if (state.filter === "completed") return todo.completed;
    return true;
  });

  return (
    <div>
      <h2>Todo 列表</h2>

      {/* 输入框区域 */}
      <input
        value={state.inputValue}
        onChange={(e) =>
          dispatch({ type: "SET_INPUT", payload: e.target.value })
        }
        placeholder="请输入待办事项"
      />
      <button onClick={() => dispatch({ type: "ADD_TODO" })}>添加</button>

      {/* 筛选按钮区域 */}
      <div>
        <button
          onClick={() => dispatch({ type: "SET_FILTER", payload: "all" })}
        >
          全部
        </button>
        <button
          onClick={() => dispatch({ type: "SET_FILTER", payload: "active" })}
        >
          未完成
        </button>
        <button
          onClick={() => dispatch({ type: "SET_FILTER", payload: "completed" })}
        >
          已完成
        </button>
      </div>

      {/* 列表展示区域 */}
      <ul>
        {filteredTodos.map((todo) => (
          <li key={todo.id}>
            <span
              style={{
                textDecoration: todo.completed ? "line-through" : "none",
              }}
            >
              {todo.text}
            </span>
            <button
              onClick={() =>
                dispatch({ type: "TOGGLE_TODO", payload: todo.id })
              }
            >
              {todo.completed ? "取消完成" : "完成"}
            </button>
            <button
              onClick={() =>
                dispatch({ type: "DELETE_TODO", payload: todo.id })
              }
            >
              删除
            </button>
          </li>
        ))}
      </ul>
    </div>
  );
}

export default TodoApp;

关键设计点解读

设计点 说明
一个 state 对象管理多个字段 避免多个 useState 分散导致逻辑混乱
action.payload 传递额外数据 TOGGLE_TODO 需要知道切换哪个 id
派生状态 filteredTodos 不需要把过滤后的结果存进 state,直接基于 state 计算即可
返回新对象 每次更新都返回 { ...state, ... },不修改原对象

高亮提示filtermap 等数组方法天然返回新数组,是写 reducer 时的好朋友。


2.3 惰性初始化 init 的用法与性能优化

当初始状态需要复杂计算(比如从 localStorage 读取、格式化数据)时,可以把计算逻辑放到 init 函数中。

错误写法:每次渲染都重新计算

// 每次组件渲染都会执行 localStorage.getItem,造成不必要的性能开销
const [state, dispatch] = useReducer(
  reducer,
  JSON.parse(localStorage.getItem("todoState")) || initState,
);

正确写法:使用 init 函数惰性初始化

// 定义一个 init 函数,只在组件挂载时执行一次
function init(initialState) {
  // 尝试从 localStorage 恢复状态
  const saved = localStorage.getItem("todoState");
  if (saved) {
    try {
      return JSON.parse(saved);
    } catch (e) {
      console.error("恢复状态失败", e);
    }
  }
  return initialState;
}

// 使用第三个参数传入 init 函数
const [state, dispatch] = useReducer(reducer, initState, init);

完整示例:带持久化的 Todo 状态

import { useReducer, useEffect } from "react";

const initState = {
  todos: [],
  inputValue: "",
  filter: "all",
};

function reducer(state, action) {
  switch (action.type) {
    case "SET_INPUT":
      return { ...state, inputValue: action.payload };
    case "ADD_TODO": {
      if (!state.inputValue.trim()) return state;
      const newTodo = {
        id: Date.now(),
        text: state.inputValue,
        completed: false,
      };
      return {
        ...state,
        todos: [...state.todos, newTodo],
        inputValue: "",
      };
    }
    case "TOGGLE_TODO":
      return {
        ...state,
        todos: state.todos.map((todo) =>
          todo.id === action.payload
            ? { ...todo, completed: !todo.completed }
            : todo,
        ),
      };
    case "DELETE_TODO":
      return {
        ...state,
        todos: state.todos.filter((todo) => todo.id !== action.payload),
      };
    case "SET_FILTER":
      return { ...state, filter: action.payload };
    default:
      return state;
  }
}

// init 函数:组件挂载时执行一次,恢复 localStorage 中的状态
function init(initialState) {
  const saved = localStorage.getItem("todoState");
  if (saved) {
    try {
      return JSON.parse(saved);
    } catch (e) {
      console.error("解析保存的状态失败", e);
    }
  }
  return initialState;
}

function TodoWithPersistence() {
  const [state, dispatch] = useReducer(reducer, initState, init);

  // 每次状态变化都保存到 localStorage
  useEffect(() => {
    localStorage.setItem("todoState", JSON.stringify(state));
  }, [state]);

  return (
    <div>
      <p>已保存的待办数量:{state.todos.length}</p>
      <input
        value={state.inputValue}
        onChange={(e) =>
          dispatch({ type: "SET_INPUT", payload: e.target.value })
        }
      />
      <button onClick={() => dispatch({ type: "ADD_TODO" })}>添加</button>
      <ul>
        {state.todos.map((todo) => (
          <li key={todo.id}>
            {todo.text} - {todo.completed ? "已完成" : "未完成"}
          </li>
        ))}
      </ul>
    </div>
  );
}

export default TodoWithPersistence;

高亮提示init 函数只会在组件挂载时执行一次,后续渲染不会再调用。它是避免重复计算和恢复持久化状态的标准做法。


2.4 useReducer + useContext:实现跨组件状态共享

在企业级项目中,我们经常需要把状态提升到多个组件共享的层级。useReducer 配合 useContext 是实现“轻量级全局状态管理”的经典方案。

步骤 1:创建 Context

import { createContext, useContext, useReducer } from "react";

// 1. 创建状态 Context,用于传递 state
const TodoStateContext = createContext(null);

// 2. 创建 dispatch Context,用于传递 dispatch 方法
const TodoDispatchContext = createContext(null);

高亮提示:把 statedispatch 分成两个 Context 是为了避免“状态更新时所有消费组件都重新渲染”的性能问题。

步骤 2:定义 reducer 和 Provider

const initState = {
  todos: [],
  inputValue: "",
  filter: "all",
};

function reducer(state, action) {
  switch (action.type) {
    case "SET_INPUT":
      return { ...state, inputValue: action.payload };
    case "ADD_TODO": {
      if (!state.inputValue.trim()) return state;
      const newTodo = {
        id: Date.now(),
        text: state.inputValue,
        completed: false,
      };
      return {
        ...state,
        todos: [...state.todos, newTodo],
        inputValue: "",
      };
    }
    case "TOGGLE_TODO":
      return {
        ...state,
        todos: state.todos.map((todo) =>
          todo.id === action.payload
            ? { ...todo, completed: !todo.completed }
            : todo,
        ),
      };
    default:
      return state;
  }
}

// Provider 组件:包裹需要共享状态的所有子组件
function TodoProvider({ children }) {
  const [state, dispatch] = useReducer(reducer, initState);

  return (
    <TodoStateContext.Provider value={state}>
      <TodoDispatchContext.Provider value={dispatch}>
        {children}
      </TodoDispatchContext.Provider>
    </TodoStateContext.Provider>
  );
}

步骤 3:自定义 Hook 读取 Context

// 读取状态
function useTodoState() {
  const context = useContext(TodoStateContext);
  if (context === null) {
    throw new Error("useTodoState 必须在 TodoProvider 内部使用");
  }
  return context;
}

// 读取 dispatch
function useTodoDispatch() {
  const context = useContext(TodoDispatchContext);
  if (context === null) {
    throw new Error("useTodoDispatch 必须在 TodoProvider 内部使用");
  }
  return context;
}

步骤 4:在任意子组件中使用

function TodoInput() {
  const state = useTodoState();
  const dispatch = useTodoDispatch();

  return (
    <div>
      <input
        value={state.inputValue}
        onChange={(e) =>
          dispatch({ type: "SET_INPUT", payload: e.target.value })
        }
      />
      <button onClick={() => dispatch({ type: "ADD_TODO" })}>添加</button>
    </div>
  );
}

function TodoList() {
  const state = useTodoState();
  // 只读取 dispatch 的子组件可以单独使用 useTodoDispatch,避免 state 变化导致重渲染
  const dispatch = useTodoDispatch();

  return (
    <ul>
      {state.todos.map((todo) => (
        <li key={todo.id}>
          {todo.text}
          <button
            onClick={() => dispatch({ type: "TOGGLE_TODO", payload: todo.id })}
          >
            {todo.completed ? "取消完成" : "完成"}
          </button>
        </li>
      ))}
    </ul>
  );
}

function App() {
  return (
    <TodoProvider>
      <h1>共享 Todo 列表</h1>
      <TodoInput />
      <TodoList />
    </TodoProvider>
  );
}

export default App;

企业级落地的几个常见做法

做法 说明
拆分为 StateContextDispatchContext 避免 dispatch 变化导致无关组件重渲染
封装自定义 Hook 隐藏 useContext 细节,使用更方便
在 Hook 中做非空校验 防止在 Provider 外部误用
状态尽量精简 只放真正需要共享的状态,不要把所有状态都塞到 Context

使用模块小结

  • useReducer 的使用流程分四步:定义 initState → 编写 reducer → 注册 useReducer → 在 UI 中调用 dispatch
  • Counter 示例展示了最基础用法;Todo 示例展示了多状态、有关联的状态管理。
  • init 函数用于惰性初始化,避免复杂初始状态在每次渲染时重复计算,适合持久化恢复场景。
  • useReducer + useContext 可以实现轻量级跨组件状态共享,企业级项目中建议拆分 State 和 Dispatch 两个 Context。

三、注意事项模块:避免踩坑

3.1 核心约束:reducer 必须是纯函数

纯函数的三个要求

  1. 不能修改参数:不能直接修改 stateaction
  2. 不能产生副作用:不能调用接口、修改 DOM、使用 Date.now() 生成状态等。
  3. 相同输入必须返回相同输出:给定同样的 stateaction,必须返回同样的新状态。

错误示例 1:在 reducer 中直接修改原 state

function reducer(state, action) {
  switch (action.type) {
    case "INCREMENT":
      // ❌ 错误:直接修改原对象
      state.count += 1;
      return state;
    default:
      return state;
  }
}

错误原因:React 通过比较前后状态的引用是否变化来决定是否重新渲染。如果你直接修改 state 再返回它,引用没变,React 可能跳过渲染,导致界面不更新。

正确写法

function reducer(state, action) {
  switch (action.type) {
    case "INCREMENT":
      // ✅ 正确:返回新对象
      return { ...state, count: state.count + 1 };
    default:
      return state;
  }
}

错误示例 2:在 reducer 中调用接口

function reducer(state, action) {
  switch (action.type) {
    case "FETCH_USER":
      // ❌ 错误:reducer 中产生副作用
      fetch("/api/user").then((res) => res.json());
      return state;
    default:
      return state;
  }
}

错误原因:reducer 必须是纯函数,调用接口会破坏纯函数要求,也让状态流转不可预测。

正确写法

// 在组件或自定义 Hook 中处理副作用
function UserProfile() {
  const [state, dispatch] = useReducer(userReducer, initState);

  useEffect(() => {
    fetch("/api/user")
      .then((res) => res.json())
      .then((data) => dispatch({ type: "SET_USER", payload: data }));
  }, []);

  return <div>{state.user.name}</div>;
}

3.2 action 的规范要求

action 对象必须携带 type 字段,这是 reducer 判断“做什么”的依据。建议用常量或 TypeScript 字面量类型来约束合法取值。

错误示例:dispatch 非法参数

// ❌ 错误:type 拼写错误,reducer 无法匹配到任何 case
dispatch({ type: "incremet" });

// ❌ 错误:缺少 type 字段
dispatch({ payload: 10 });

// ❌ 错误:type 是 undefined
dispatch({ type: undefined });

正确做法:用常量约束 type

// 集中定义所有 action 类型
const ActionTypes = {
  INCREMENT: "INCREMENT",
  DECREMENT: "DECREMENT",
  RESET: "RESET",
};

function reducer(state, action) {
  switch (action.type) {
    case ActionTypes.INCREMENT:
      return { count: state.count + 1 };
    case ActionTypes.DECREMENT:
      return { count: state.count - 1 };
    case ActionTypes.RESET:
      return { count: 0 };
    default:
      return state;
  }
}

// 使用常量 dispatch,避免拼写错误
dispatch({ type: ActionTypes.INCREMENT });

TypeScript 中约束 action 类型

type Action =
  | { type: "INCREMENT" }
  | { type: "DECREMENT" }
  | { type: "SET_COUNT"; payload: number };

function reducer(state: State, action: Action): State {
  // TypeScript 会在编译阶段检查 type 是否合法
}

高亮提示:TypeScript 的联合类型会在你写错 type 时直接报错,是团队项目中最推荐的做法。


3.3 状态更新的不可变性

React 的状态更新机制依赖“不可变性”(Immutability)。也就是说,每次更新都要返回新的对象或数组,而不是修改原来的。

错误示例:直接修改数组

function reducer(state, action) {
  switch (action.type) {
    case "ADD_TODO":
      // ❌ 错误:push 会修改原数组
      state.todos.push(action.payload);
      return state;
    default:
      return state;
  }
}

正确写法:使用展开运算符

function reducer(state, action) {
  switch (action.type) {
    case "ADD_TODO":
      // ✅ 正确:返回新数组
      return { ...state, todos: [...state.todos, action.payload] };
    default:
      return state;
  }
}

使用 Immer 简化不可变更新

当状态嵌套很深时,展开运算符会非常啰嗦。可以用 immer 库让代码写起来像“直接修改”一样,但内部仍然保持不可变。

npm install immer
import { useImmerReducer } from "use-immer";
// 或者使用 immer 的 produce 配合 useReducer
import { useImmerReducer } from "use-immer";

const initState = {
  user: {
    profile: {
      name: "",
      address: {
        city: "",
      },
    },
  },
};

function reducer(draft, action) {
  switch (action.type) {
    case "SET_NAME":
      // 看起来像直接修改,但 Immer 会在内部生成不可变的新状态
      draft.user.profile.name = action.payload;
      break;
    case "SET_CITY":
      draft.user.profile.address.city = action.payload;
      break;
  }
}

function DeepNestedForm() {
  const [state, dispatch] = useImmerReducer(reducer, initState);

  return (
    <div>
      <p>姓名:{state.user.profile.name}</p>
      <p>城市:{state.user.profile.address.city}</p>
      <button onClick={() => dispatch({ type: "SET_NAME", payload: "张三" })}>
        设置姓名
      </button>
    </div>
  );
}

高亮提示useImmerReduceruseReducer + immer 的封装,reducer 的第一个参数是 draft(草稿),你可以像直接修改对象一样写代码,但状态依然是不可变的。


3.4 性能优化注意点

dispatch 的稳定性

React 保证:同一个组件内,dispatch 函数的引用永远不会变化。所以你可以放心地把 dispatch 传给子组件,不需要用 useCallback 包裹。

function Parent() {
  const [state, dispatch] = useReducer(reducer, initState);

  // dispatch 引用稳定,直接传给子组件即可
  return <Child dispatch={dispatch} />;
}

配合 useMemo 缓存派生状态

如果基于 state 的派生计算很耗时,使用 useMemo

import { useMemo } from "react";

function TodoList({ state }) {
  // 只有当 state.todos 或 state.filter 变化时才重新计算
  const filteredTodos = useMemo(() => {
    return state.todos.filter((todo) => {
      if (state.filter === "active") return !todo.completed;
      if (state.filter === "completed") return todo.completed;
      return true;
    });
  }, [state.todos, state.filter]);

  return (
    <ul>
      {filteredTodos.map((todo) => (
        <li key={todo.id}>{todo.text}</li>
      ))}
    </ul>
  );
}

配合 useCallback 缓存事件处理函数

如果子组件使用了 React.memo,父组件传入的回调函数需要保持稳定,这时用 useCallback

import { useCallback } from "react";

function TodoItem({ todo, dispatch }) {
  // useCallback 让 handleToggle 在依赖不变时保持引用稳定
  const handleToggle = useCallback(() => {
    dispatch({ type: "TOGGLE_TODO", payload: todo.id });
  }, [dispatch, todo.id]);

  return (
    <li>
      <button onClick={handleToggle}>
        {todo.completed ? "未完成" : "完成"}
      </button>
      {todo.text}
    </li>
  );
}

高亮提示dispatch 本身不需要 useCallback,但事件处理函数中如果依赖了 todo.id 等变量,可以用 useCallback 优化,避免 React.memo 失效。


3.5 React 19 中 useReducer 的专属注意事项

自动批处理(Automatic Batching)

在 React 19 中,即使不在 React 事件处理函数中,多次 dispatch 也会被自动批处理为一次渲染。

function handleClick() {
  // 在 React 19 中,下面两次 dispatch 只会触发一次重新渲染
  dispatch({ type: "INCREMENT" });
  dispatch({ type: "SET_NAME", payload: "Tom" });
}

高亮提示:这意味着你不需要刻意合并多个 dispatch,React 19 会自动帮你优化。

并发渲染(Concurrent Rendering)

React 19 引入了并发渲染,某些更新可以被中断或延迟。这要求你在 reducer 中:

  1. 绝对不能有副作用:因为 render 可能被重复执行或中断。
  2. 状态更新必须基于旧状态:避免使用闭包中的旧值,应使用 state 参数。
// ✅ 正确:基于 reducer 传入的 state
function reducer(state, action) {
  if (action.type === "INCREMENT") {
    return { count: state.count + 1 };
  }
}

// ❌ 错误:依赖组件闭包中的 count,可能在并发渲染下拿到过期值
function BadComponent() {
  const [count, setCount] = useState(0);
  // 在 useReducer 中同样要避免这种模式
}

Suspense 与 useReducer

如果你的组件使用了 Suspense 和异步数据流,确保 reducer 中的状态只依赖同步数据,异步副作用放在 useEffect 或数据获取库中处理。


3.6 常见踩坑汇总

错误 错误代码 错误原因 正确代码
忘记在 reducer 中返回新状态 case 'ADD': state.count++; break; 直接修改 state,引用不变导致不渲染 case 'ADD': return { ...state, count: state.count + 1 };
错误调用 dispatch dispatch('INCREMENT') dispatch 参数必须是对象 dispatch({ type: 'INCREMENT' })
初始状态传值错误 useReducer(reducer, { count: 0 }) 在需要函数时 初始状态复杂时应使用 init 函数 useReducer(reducer, initArg, init)
action 缺少 type dispatch({ payload: 1 }) reducer 无法判断意图 dispatch({ type: 'SET', payload: 1 })
在 reducer 中调用 setState case 'X': setState(...); return state; reducer 中不能有副作用 在组件或 useEffect 中处理副作用
dispatch 拼写错误 dispatch({ type: 'INCREMNET' }) 无法匹配任何 case 使用常量或 TS 约束 type
忽略 default 分支 没有 default 收到未知 action 可能报错 始终返回 return state;
深层状态用展开运算符硬写 多层 ... 嵌套 代码难以维护,容易遗漏 使用 Immer 简化

注意事项模块小结

  • reducer 必须是纯函数:不修改参数、无副作用、相同输入相同输出。
  • action 必须携带 type,建议用常量或 TypeScript 联合类型约束。
  • 状态更新必须保持不可变,可用展开运算符或 Immer 库简化。
  • dispatch 引用稳定,可直接传递;复杂派生计算用 useMemo,回调函数用 useCallback
  • React 19 中 useReducer 支持自动批处理和并发渲染,要求 reducer 更加纯粹。
  • 常见错误集中在“直接修改 state”、“action 格式错误”、“reducer 中写副作用”三类。

四、常见问题答疑(10 个初学者最关心的问题)

Q1:useReducer 能完全替代 useState 吗?

:能,但不推荐。简单状态用 useState 更简洁;复杂、多分支、多关联状态用 useReducer 更清晰。它们是互补关系。

Q2:reducer 里的 state 和组件里的 state 是同一个吗?

:是的。useReducer 返回的 state 就是 reducer 最近一次执行后返回的新状态。reducer 接收的 state 永远是当前最新的状态。

Q3:为什么 action 一定要有 type

type 是 action 的“身份证”。reducer 靠 type 判断要执行哪种状态更新。没有 type,reducer 就不知道你想做什么。

Q4:我可以在 reducer 中调用 localStorage.setItem 吗?

:不可以。localStorage 操作是副作用,应该放在 useEffect 中处理。reducer 必须保持纯函数。

Q5:dispatch 后的状态为什么没有立刻变?

:React 的状态更新是异步的。dispatch 只是向 React 提交一个更新请求,React 会在合适的时机调用 reducer 并重新渲染组件。

Q6:useReducer 和 Redux 有什么关系?

:它们的概念很像:都有 reducer、action、dispatch。但 useReducer 是 React 内置的组件级状态管理工具,Redux 是独立的全局状态管理库。你可以把 useReducer 理解为“组件内的 Redux 思想”。

Q7:为什么我的状态修改了,界面却不更新?

:最常见的原因是你在 reducer 中直接修改了原 state 并返回它。例如 state.count++return state。由于引用没变,React 认为状态没有变化,跳过渲染。要返回新对象。

Q8:初始状态是一个复杂对象,怎么避免每次渲染重复创建?

:使用 useReducer 的第三个参数 init 函数。init 只在组件挂载时执行一次,后续渲染不会重复调用。

Q9:多个组件怎么共享同一个 useReducer 的状态?

:把 useReducer 放在父组件或 Provider 中,然后通过 props 或 useContextstatedispatch 传给子组件。企业级常用 useReducer + useContext 组合。

Q10:useReducer 中的 dispatch 需要用 useCallback 包裹吗?

:不需要。React 保证同一个组件内 dispatch 的引用永远稳定,可以直接传递给子组件,不会导致子组件不必要重渲染。


结语

useReducer 是 React 中处理复杂状态的利器。它的核心思想其实很简单:

UI 发命令(dispatch),reducer 决定新状态(new state)。

掌握这个流程后,你会发现无论是计数器、表单、待办列表,还是更复杂的企业级应用状态管理,useReducer 都能让你的代码结构更清晰、可维护性更强。

0

评论区