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 内部会做以下事情:
- 把 action 存入更新队列:React 不会立即修改状态,而是先把这次更新请求记录下来。
- 触发重新调度:React 19 引入的并发渲染机制会让更新可以被中断、优先级重新排序。
- 在合适的时机执行 reducer:React 取出当前状态
state和队列中的action,调用reducer(state, action)得到新状态。 - 比较并渲染:如果新状态与旧状态引用不同,React 会重新渲染组件。
- 批处理优化: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, ... },不修改原对象 |
高亮提示:
filter、map等数组方法天然返回新数组,是写 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);
高亮提示:把
state和dispatch分成两个 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;
企业级落地的几个常见做法
| 做法 | 说明 |
|---|---|
拆分为 StateContext 和 DispatchContext |
避免 dispatch 变化导致无关组件重渲染 |
| 封装自定义 Hook | 隐藏 useContext 细节,使用更方便 |
| 在 Hook 中做非空校验 | 防止在 Provider 外部误用 |
| 状态尽量精简 | 只放真正需要共享的状态,不要把所有状态都塞到 Context |
使用模块小结
useReducer的使用流程分四步:定义initState→ 编写reducer→ 注册useReducer→ 在 UI 中调用dispatch。- Counter 示例展示了最基础用法;Todo 示例展示了多状态、有关联的状态管理。
init函数用于惰性初始化,避免复杂初始状态在每次渲染时重复计算,适合持久化恢复场景。useReducer+useContext可以实现轻量级跨组件状态共享,企业级项目中建议拆分 State 和 Dispatch 两个 Context。
三、注意事项模块:避免踩坑
3.1 核心约束:reducer 必须是纯函数
纯函数的三个要求:
- 不能修改参数:不能直接修改
state或action。 - 不能产生副作用:不能调用接口、修改 DOM、使用
Date.now()生成状态等。 - 相同输入必须返回相同输出:给定同样的
state和action,必须返回同样的新状态。
错误示例 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>
);
}
高亮提示:
useImmerReducer是useReducer+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 中:
- 绝对不能有副作用:因为 render 可能被重复执行或中断。
- 状态更新必须基于旧状态:避免使用闭包中的旧值,应使用
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 或 useContext 把 state 和 dispatch 传给子组件。企业级常用 useReducer + useContext 组合。
Q10:useReducer 中的 dispatch 需要用 useCallback 包裹吗?
答:不需要。React 保证同一个组件内 dispatch 的引用永远稳定,可以直接传递给子组件,不会导致子组件不必要重渲染。
结语
useReducer 是 React 中处理复杂状态的利器。它的核心思想其实很简单:
UI 发命令(dispatch),reducer 决定新状态(new state)。
掌握这个流程后,你会发现无论是计数器、表单、待办列表,还是更复杂的企业级应用状态管理,useReducer 都能让你的代码结构更清晰、可维护性更强。
评论区