WinddSnow

React-useReducer-and-State-Logic-Extraction

字数统计: 4.2k阅读时长: 18 min
2026/08/01

第94课:useReducer 与状态逻辑抽离——Reducer 模式、与 Redux 的关系、复杂表单状态管理

当组件状态逻辑变得复杂——例如包含多个子值需要协调更新、下一次状态依赖上一次状态、或者状态转换涉及多种操作类型——useState 的简单“设置值”模式会变得难以维护。React 提供了 useReducer Hook,它将状态更新逻辑从组件中抽离到一个独立的纯函数(Reducer)中,通过派发 Action 来描述“发生了什么”,由 Reducer 统一决定“状态如何变化”。这种模式与 Redux 的核心思想一脉相承,但在组件级别轻量得多。理解 Reducer 模式不仅能帮助你管理复杂组件状态,还为学习 Redux 等全局状态管理库打下坚实的基础。


1. useReducer 的基本语法与工作原理

useReducer 接收三个参数:Reducer 函数初始状态,以及一个可选的惰性初始化函数。它返回一个包含两个元素的数组:当前状态dispatch 函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import { useReducer } from 'react';

// 1. 定义 Reducer:纯函数,接收 state 和 action,返回新的 state
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: action.payload ?? 0 };
default:
return state; // 重要:未知 action 必须返回原 state
}
}

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

return (
<div>
<p>Count: {state.count}</p>
<button onClick={() => dispatch({ type: 'increment' })}>+1</button>
<button onClick={() => dispatch({ type: 'decrement' })}>-1</button>
<button onClick={() => dispatch({ type: 'reset', payload: 0 })}>重置</button>
</div>
);
}

1.1 Reducer 函数的契约

Reducer 是一个纯函数,它严格遵守以下规则:

  • 接收两个参数(state, action)state 是当前状态,action 是描述变更的普通对象。
  • 必须返回一个新的 state(不能修改原 state)。
  • 不能有副作用——不能在 Reducer 中发起网络请求、修改 DOM、调用 setTimeout
  • 对于未知的 action.type,必须返回原 statedefault 分支)。

1.2 Action 的约定

Action 是一个描述“发生了什么”的普通对象。社区约定 Action 对象必须有一个 type 字段(字符串),以及一个可选的 payload 字段携带数据。

1
2
3
4
// Action 示例
{ type: 'increment' }
{ type: 'add_todo', payload: { id: 1, text: 'Buy milk' } }
{ type: 'update_user', payload: { name: 'Alice', age: 30 } }

1.3 dispatch 的行为

dispatch 函数接收一个 Action 对象,React 会调用你提供的 Reducer 函数,传入当前 state 和该 action,然后用 Reducer 的返回值作为新的 state。与类组件的 setState 类似,React 会批量处理同一个事件处理函数中的多个 dispatch 调用,仅触发一次重新渲染。

1
2
3
4
5
6
function handleTripleIncrement() {
dispatch({ type: 'increment' });
dispatch({ type: 'increment' });
dispatch({ type: 'increment' });
// 三次 dispatch 被合并为一次渲染,count 增加 3
}

2. useReducer vs useState:如何选择?

场景 推荐 Hook
单个独立的基本类型状态(布尔、数字、字符串) useState
状态更新逻辑简单(如 setX(x + 1) useState
多个状态值经常一起更新(如表单多字段) useReducer
下一次状态依赖上一次状态的复杂逻辑 useReducer
需要将更新逻辑抽离为独立函数以进行单元测试 useReducer
状态转换涉及多种操作类型(如购物车的增删改清空) useReducer

核心判断标准:当你在 useState 中开始写 setState(prev => { ... 复杂逻辑 ... }),或者发现多个 useState 调用之间需要协调同步时,就是切换到 useReducer 的信号。


3. Reducer 模式与 Redux 的关系

useReducer 的 API 设计与 Redux 的核心概念高度相似——事实上,Redux 本身就是一个在全局级别工作的 Reducer 管理模式。两者都遵循单一数据源状态只读通过纯函数修改状态三大原则。

概念 useReducer Redux
状态存储 组件内部(或通过 Context 共享) 全局 Store
Reducer 函数,接收 (state, action) 相同,但通常是组合后的 Root Reducer
dispatch 组件内使用 全局可用(通过 useDispatch
中间件 不支持(需自己实现) 内置中间件支持(redux-thunk 等)
DevTools 不支持 Redux DevTools 支持时间旅行调试
适用场景 组件级或小范围共享的复杂状态 应用级全局状态管理

学习路径:先熟练掌握 useReducer 管理组件状态,理解 Reducer 纯函数和 Action 派发的思想。当你需要跨多个不相关的组件共享状态、需要中间件处理异步、或需要时间旅行调试时,再引入 Redux(或 Zustand 等轻量替代)。useReducer 是 Redux 的微缩版——掌握了它,Redux 只是加上了全局 Store 和中间件。


4. 复杂表单状态管理:useReducer 的实战优势

表单是 useReducer 最能发挥优势的场景之一。一个典型的复杂表单包含多个字段、验证规则、提交状态、错误信息等——如果使用多个 useState,每个字段各一个,加上 loadingerrors,代码会迅速膨胀且字段间的联动逻辑难以维护。使用 useReducer 可以将所有表单状态和变更逻辑集中在一个 Reducer 中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
import { useReducer } from 'react';

// 初始状态
const initialFormState = {
values: { name: '', email: '', password: '', confirmPassword: '' },
errors: {},
touched: {},
isSubmitting: false,
};

// Reducer:统一管理所有表单状态
function formReducer(state, action) {
switch (action.type) {
case 'field_change': {
const { name, value } = action.payload;
return {
...state,
values: { ...state.values, [name]: value },
touched: { ...state.touched, [name]: true },
};
}
case 'validation_error': {
return { ...state, errors: action.payload };
}
case 'submit_start': {
return { ...state, isSubmitting: true, errors: {} };
}
case 'submit_success': {
return { ...initialFormState }; // 重置表单
}
case 'submit_failure': {
return { ...state, isSubmitting: false, errors: action.payload };
}
default:
return state;
}
}

// 验证函数(与 Reducer 配合使用)
function validateForm(values) {
const errors = {};
if (!values.name.trim()) errors.name = '姓名不能为空';
if (!values.email.includes('@')) errors.email = '请输入有效的邮箱';
if (values.password.length < 8) errors.password = '密码至少 8 位';
if (values.password !== values.confirmPassword) {
errors.confirmPassword = '两次密码不一致';
}
return errors;
}

function SignupForm() {
const [state, dispatch] = useReducer(formReducer, initialFormState);

function handleChange(e) {
dispatch({
type: 'field_change',
payload: { name: e.target.name, value: e.target.value },
});
}

async function handleSubmit(e) {
e.preventDefault();
const errors = validateForm(state.values);
if (Object.keys(errors).length > 0) {
dispatch({ type: 'validation_error', payload: errors });
return;
}
dispatch({ type: 'submit_start' });
try {
await fetch('/api/register', {
method: 'POST',
body: JSON.stringify(state.values),
});
dispatch({ type: 'submit_success' });
alert('注册成功!');
} catch {
dispatch({ type: 'submit_failure', payload: { general: '提交失败,请重试' } });
}
}

const { values, errors, touched, isSubmitting } = state;

return (
<form onSubmit={handleSubmit}>
<div>
<input name="name" value={values.name} onChange={handleChange} placeholder="姓名" />
{touched.name && errors.name && <span style={{ color: 'red' }}>{errors.name}</span>}
</div>
<div>
<input name="email" value={values.email} onChange={handleChange} placeholder="邮箱" />
{touched.email && errors.email && <span style={{ color: 'red' }}>{errors.email}</span>}
</div>
<div>
<input name="password" type="password" value={values.password} onChange={handleChange} placeholder="密码" />
{touched.password && errors.password && <span style={{ color: 'red' }}>{errors.password}</span>}
</div>
<div>
<input name="confirmPassword" type="password" value={values.confirmPassword} onChange={handleChange} placeholder="确认密码" />
{touched.confirmPassword && errors.confirmPassword && <span style={{ color: 'red' }}>{errors.confirmPassword}</span>}
</div>
<button type="submit" disabled={isSubmitting}>
{isSubmitting ? '提交中...' : '注册'}
</button>
{errors.general && <p style={{ color: 'red' }}>{errors.general}</p>}
</form>
);
}

设计要点

  • 一个 Reducer 管理所有表单相关状态(字段值、错误、触摸状态、提交状态),替代了至少 6 个 useState 调用。
  • Action 类型清晰地表达了状态变更的意图(field_changesubmit_start 等),便于调试和维护。
  • 验证逻辑独立于 Reducer,Reducer 只负责“如何更新状态”,验证函数负责“判断是否有错误”。
  • 提交成功后通过返回 initialFormState 来重置整个表单,一行代码完成所有字段清空和状态重置。

5. 惰性初始化:通过第三个参数计算初始状态

useReducer 的第三个参数是一个惰性初始化函数。如果初始状态需要通过 Props 或复杂计算获得,传入这个函数可以避免在每次渲染时都重新计算。

1
2
3
4
5
6
7
8
function createInitialState(initialCount) {
return { count: initialCount, history: [] };
}

function Counter({ initialCount }) {
const [state, dispatch] = useReducer(reducer, initialCount, createInitialState);
// 等价于:useReducer(reducer, createInitialState(initialCount)),但惰性版本仅在首次渲染时调用 createInitialState
}

惰性初始化函数接收第二个参数(initialCount)并返回初始状态。这与 useState(() => expensive()) 的惰性模式一致。


6. 将 Reducer 与 Context 结合:轻量级的全局状态管理

当多个不相关的组件需要访问同一个 Reducer 管理的状态时,可以将 useReducer 与 React Context 结合,构建一个不依赖第三方库的全局状态管理方案。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
import { createContext, useContext, useReducer } from 'react';

// 1. 创建 Context
const TasksContext = createContext(null);
const TasksDispatchContext = createContext(null);

// 2. Reducer
function tasksReducer(state, action) {
switch (action.type) {
case 'add':
return [...state, { id: Date.now(), text: action.payload, done: false }];
case 'toggle':
return state.map(t => t.id === action.payload ? { ...t, done: !t.done } : t);
case 'delete':
return state.filter(t => t.id !== action.payload);
default:
return state;
}
}

// 3. Provider 组件
function TasksProvider({ children }) {
const [tasks, dispatch] = useReducer(tasksReducer, []);
return (
<TasksContext.Provider value={tasks}>
<TasksDispatchContext.Provider value={dispatch}>
{children}
</TasksDispatchContext.Provider>
</TasksContext.Provider>
);
}

// 4. 自定义 Hooks
function useTasks() {
return useContext(TasksContext);
}
function useTasksDispatch() {
return useContext(TasksDispatchContext);
}

// 5. 在任意子组件中使用
function TaskList() {
const tasks = useTasks();
return <ul>{tasks.map(t => <li key={t.id}>{t.text}</li>)}</ul>;
}

function AddTask() {
const dispatch = useTasksDispatch();
return <button onClick={() => dispatch({ type: 'add', payload: 'New Task' })}>添加任务</button>;
}

与 Redux 的对比:这种模式在小型应用中完全够用,无需安装任何依赖。但缺少中间件(如异步处理)、DevTools 集成和社区生态。当应用状态逻辑进一步复杂化时,迁移到 Redux Toolkit 或 Zustand 是自然的升级路径。


课后练习

一、概念自测(选择题 / 填空题)

  1. (单选) useReducer 返回的 dispatch 函数的作用是什么?
    A. 直接修改当前状态。
    B. 派发一个 Action 对象,React 调用 Reducer 计算新状态。
    C. 重置 Reducer 为初始状态。
    D. 订阅全局状态变化。

  2. (单选) 关于 Reducer 函数的描述,哪项是错误的?
    A. Reducer 必须是纯函数。
    B. Reducer 可以修改传入的 state 参数以提高性能。
    C. Reducer 对于未知的 action.type 应返回原 state
    D. Reducer 接收 (state, action) 两个参数。

  3. (填空) 使用 useReducer 时,要将多个字段的更新合并到状态中,通常需要用到展开运算符 ...state 来创建 ______ 的 state 对象。

  4. (多选) 以下哪些场景更适合使用 useReducer 而非 useState
    A. 管理一个包含 5 个字段的表单状态。
    B. 一个简单的计数器(仅需要 +1 和 -1)。
    C. 购物车,包含添加、删除、修改数量、清空等操作。
    D. 需要将状态更新逻辑抽离为独立函数进行单元测试。

二、AI 编程任务:编写面向 AI 的提示词

场景:你需要实现一个待办事项列表,使用 useReducer 管理状态。要求如下:

  • 状态为 { todos: Array<{ id, text, completed }>, filter: 'all' | 'active' | 'completed' }
  • Action 类型包括:add_todo(携带 text)、toggle_todo(携带 id)、delete_todo(携带 id)、clear_completed(清空所有已完成)、set_filter(携带 filter)。
  • 使用 useReducer 实现所有操作,并渲染输入框、待办列表和三个筛选按钮(全部/未完成/已完成)。
  • 已完成项显示删除线,点击可切换完成状态。
  • 底部统计未完成数量。

任务要求:请写出一段完整的中文提示词,发送给 AI,使其生成符合上述要求的 React 组件代码。提示词中需明确指定 Reducer 的结构、所有 Action 类型和状态变更逻辑。

三、Agent 模式下的提示词示例

你是一个资深前端开发 Agent。请创建一个使用 useReducer 管理状态的 Todo 应用组件。需要创建文件 src/components/TodoApp.jsx

  • 定义 initialState = { todos: [], filter: 'all' }
  • 定义 Reducer todoReducer(state, action),通过 switch (action.type) 处理以下类型:
    • 'add_todo':向 todos 追加 { id: Date.now(), text: action.payload, completed: false }
    • 'toggle_todo':通过 id 找到对应 todo,翻转 completed
    • 'delete_todo':通过 id 过滤掉对应 todo。
    • 'clear_completed':过滤掉所有 completedtrue 的 todo。
    • 'set_filter':设置 filteraction.payload
    • default:返回 state
  • 组件内部使用 useReducer(todoReducer, initialState) 获取 [state, dispatch]
  • 输入框通过 useState 管理本地输入值,按回车或点击按钮时 dispatch add_todo 并清空输入。
  • 列表渲染:根据 state.filter 过滤 todos,使用 map 渲染每项。每项包含复选框(checked={todo.completed})、文字(style={{ textDecoration: todo.completed ? 'line-through' : 'none' }})和删除按钮。
  • 底部:三个筛选按钮('all''active''completed'),当前选中的按钮高亮。统计 todos.filter(t => !t.completed).length
  • 为所有功能添加 JSDoc 注释,确保代码可直接运行。完成后输出完整文件内容。

四、面试真题与参考答案

题目(美团前端面试题):

请解释 React 中 useReducer 的工作原理及其与 useState 的适用场景对比。为什么说 Reducer 模式更容易进行单元测试?useReducer 与 Redux 有什么关系?能否用 useReducer 完全替代 Redux?

参考答案

useReducer 通过 dispatch(action) 触发状态更新,React 调用用户提供的 Reducer 纯函数,传入当前 state 和 action,用返回的新 state 替换旧 state。它适用于状态逻辑复杂、多个子值相互依赖、或下一次状态依赖上一次状态的场景(如表单多字段、购物车操作)。useState 更适合简单的独立状态。

Reducer 模式更容易进行单元测试的原因:Reducer 是纯函数——不依赖组件上下文、不产生副作用、输入唯一决定输出。测试时只需构造不同的 (state, action) 组合,断言返回值是否符合预期,无需渲染组件或模拟 DOM 事件。

useReducer 是 Redux 的微观版本。Redux 的核心就是一个全局的 useReducer——通过 createStore 创建一个持有整个应用状态的 Store,dispatch 将 Action 发送给 Root Reducer。两者的 Reducer 函数签名和不可变更新原则完全一致。学习 useReducer 可以直接迁移到 Redux。

useReducer 无法完全替代 Redux,因为它缺少:全局单一 Store(需要手动结合 Context)、中间件机制(异步处理需要自己封装)、DevTools 时间旅行调试、以及成熟的生态系统(如 Redux Toolkit 的 createSlice、RTK Query)。对于小到中型应用,useReducer + Context 足够;对于大型应用或团队协作项目,Redux Toolkit 提供了更完善的工程化支持。


课后练习答案

一、概念自测答案

  1. B

    • 解析:dispatch 派发 Action,React 调用 Reducer 计算新状态。它不直接修改状态,也不重置 Reducer。
  2. B

    • 解析:Reducer 必须遵循不可变原则,绝不能修改传入的 state 参数,必须返回新的状态对象。A、C、D 均为正确描述。
  3. 不可变

    • 解析:Reducer 必须返回新的 state 对象而非修改原对象。
  4. A、C、D

    • 解析:A 表单多字段联动、C 购物车多种操作、D 需要独立测试 Reducer 逻辑,均适合 useReducer。B 简单计数器使用 useState 更简洁。

二、AI 编程任务参考答案(提示词示例)

示例提示词
“请用 React 的 useReducer 实现一个 Todo 应用组件。要求:

  • 状态包含 todos 数组和 filter 字符串。
  • Reducer 处理 add_todotoggle_tododelete_todoclear_completedset_filter 五种 Action。
  • 渲染输入框、待办列表(支持切换完成状态和删除)、三个筛选按钮、未完成统计。
  • 使用不可变更新方式,添加 JSDoc 注释。输出完整组件代码。”
CATALOG
  1. 1. 第94课:useReducer 与状态逻辑抽离——Reducer 模式、与 Redux 的关系、复杂表单状态管理
    1. 1.1. 1. useReducer 的基本语法与工作原理
      1. 1.1.1. 1.1 Reducer 函数的契约
      2. 1.1.2. 1.2 Action 的约定
      3. 1.1.3. 1.3 dispatch 的行为
    2. 1.2. 2. useReducer vs useState:如何选择?
    3. 1.3. 3. Reducer 模式与 Redux 的关系
    4. 1.4. 4. 复杂表单状态管理:useReducer 的实战优势
    5. 1.5. 5. 惰性初始化:通过第三个参数计算初始状态
    6. 1.6. 6. 将 Reducer 与 Context 结合:轻量级的全局状态管理
    7. 1.7. 课后练习
      1. 1.7.1. 一、概念自测(选择题 / 填空题)
      2. 1.7.2. 二、AI 编程任务:编写面向 AI 的提示词
      3. 1.7.3. 三、Agent 模式下的提示词示例
      4. 1.7.4. 四、面试真题与参考答案
    8. 1.8. 课后练习答案
      1. 1.8.1. 一、概念自测答案
      2. 1.8.2. 二、AI 编程任务参考答案(提示词示例)