React Reducer 与 Context:从混乱到有序的状态管理
当你的 useState 开始打架,当 props 传了七八层还在继续传,说明你需要换一种方式组织状态了,原生 react 怎么去解决这种问题?
开篇:为什么需要 Reducer 和 Context
在 React 里写交互,大多数人都是从 useState 开始的。
一个输入框、一个按钮、一个开关------用 useState 完全够用。但随着功能变复杂,你会开始遇到两个很实际的问题:
问题一:状态逻辑越来越散
假设你在做一个任务列表,用户可以添加、编辑、删除任务。一开始你可能会这样写:
scss
const [tasks, setTasks] = useState(initialTasks);
function handleAddTask(text) {
setTasks([...tasks, { id: nextId++, text, done: false }]);
}
function handleChangeTask(task) {
setTasks(tasks.map(t => (t.id === task.id ? task : t)));
}
function handleDeleteTask(taskId) {
setTasks(tasks.filter(t => t.id !== taskId));
}
这能跑,但随着操作变多,逻辑会越来越分散。每个事件处理函数都在单独修改同一个 state,你很难一眼看出"某个操作到底改了什么"。
问题二:props 传了七八层
另一个常见场景:你有一个用户对象在顶层组件里,但需要在五六层深的子组件里使用。于是你不得不一层一层往下传:
xml
<Page user={user}>
<Layout user={user}>
<Sidebar user={user} />
<Content user={user}>
<Profile user={user} />
</Content>
</Layout>
</Page>
中间那些组件(Layout、Sidebar、Content)根本不关心 user,它们只是被迫当"快递员"。这就是 prop drilling------逐层透传 props。
这两个问题,React 都有原生方案解决:
- Reducer 解决"逻辑分散"
- Context 解决"传递太深"
这篇文章就是把这两个东西讲清楚。
本文会覆盖什么
useReducer核心概念和用法- Reducer 函数的设计原则
- Context 的创建、提供、消费
- Reducer + Context 组合模式
- 什么时候该用,什么时候不该用
文章的目标不是让你记住几个 API 名字,而是建立一套稳定的状态组织思维。
Part 1: useState 的局限性
在讲 Reducer 和 Context 之前,我们先看清楚 useState 到底哪里不够用。
逻辑分散问题
用 useState 管理状态时,更新逻辑是写在各个事件处理函数里的。
一个简单的表单可能长这样:
scss
const [status, setStatus] = useState('typing');
const [error, setError] = useState(null);
const [answer, setAnswer] = useState('');
async function handleSubmit(e) {
e.preventDefault();
setStatus('submitting');
setError(null);
try {
await submitForm(answer);
setStatus('success');
} catch (err) {
setError(err);
setStatus('typing');
}
}
这段代码不算复杂,但你已经能看出一个问题:状态更新逻辑散落在不同的地方。
setStatus('submitting')、setError(null)、setStatus('success')、setStatus('typing')------这些调用分散在 handleSubmit 里,如果你想知道"提交失败时到底改了哪些状态",你需要仔细读整个函数。
当操作变多、状态变复杂后,这个问题会越来越明显。你会开始在不同函数里反复调用 setXxx(...),然后越来越难看清"某个操作到底改了什么"。
prop drilling 问题
这是另一个经典问题。
假设你在做一个博客系统,用户信息存在 App 组件里,但需要在 Article 组件里显示作者头像。组件树长这样:
xml
<App>
<Layout>
<Sidebar />
<Main>
<ArticleList>
<Article />
</ArticleList>
</Main>
</Layout>
</App>
如果不做任何处理,你可能需要这样传:
xml
<App user={user}>
<Layout user={user}>
<Sidebar />
<Main user={user}>
<ArticleList user={user}>
<Article user={user} />
</ArticleList>
</Main>
</Layout>
</App>
Layout、Main、ArticleList 这三个组件根本不需要 user,它们只是被迫当"快递员"。
这种写法的问题不只是"代码难看",而是:
- 中间组件和
user产生了不必要的耦合 - 如果以后
user的结构变了,你需要改所有中间层的 props - 读代码时很难追踪数据到底从哪来
为什么需要更好的组织方式
这两个问题的根源是一样的:当应用变复杂后,useState + props 的默认方式不够用了。
useState 适合管理简单的、局部的状态。但当:
- 同一个状态会被很多事件处理函数修改
- 状态逻辑越来越长,越来越分散
- 多个组件需要读写同一份数据
- 组件层级太深,props 传得太多
你就需要更结构化的方案。
React 提供了两个原生工具:
useReducer:把状态更新逻辑集中到一个地方Context:让数据"跳过"中间层直达需要它的组件
下面我们就一个一个讲。
Part 2: Reducer 是什么
从生活例子理解 Reducer
想象你去银行办业务。
没有 Reducer 的情况:你直接走到柜台,告诉柜员"我要存 500 块"、"我要取 200 块"、"我要查余额"。每个操作都是直接的、独立的。
有 Reducer 的情况:你先填一张表,写清楚"操作类型:存款,金额:500",然后把表交给柜员。柜员根据表格内容,统一处理。
Reducer 就是那个"柜员"。它接收两个东西:
- 当前状态(
state) - 一个描述"发生了什么"的 action
然后返回一个新的状态。
javascript
function bankReducer(balance, action) {
switch (action.type) {
case 'deposit':
return balance + action.amount;
case 'withdraw':
return balance - action.amount;
default:
throw Error('Unknown action: ' + action.type);
}
}
Reducer 的三个要素
每个 Reducer 都有三个核心概念:
| 概念 | 含义 | 例子 |
|---|---|---|
| state | 当前状态 | { balance: 1000 } |
| action | 描述"发生了什么"的对象 | { type: 'deposit', amount: 500 } |
| dispatch | 触发状态更新的函数 | dispatch({ type: 'deposit', amount: 500 }) |
流程是这样的:
scss
用户操作 → dispatch(action) → Reducer(state, action) → 新的 state
纯函数原则
Reducer 有一个重要约束:它必须是纯函数。
什么是纯函数?简单说就是:
- 同样的输入,永远得到同样的输出
- 不会修改外部变量
- 不会发请求、不会改 DOM、不会做任何"副作用"
javascript
// ❌ 不是纯函数:修改了外部变量
let total = 0;
function badReducer(state, action) {
total += action.amount; // 副作用!
return { ...state, balance: state.balance + action.amount };
}
// ✅ 纯函数:只根据输入计算输出
function goodReducer(state, action) {
return { ...state, balance: state.balance + action.amount };
}
为什么必须是纯函数?因为 React 需要能够:
- 随时重新执行 reducer 来计算新状态
- 在开发模式下多次执行来检测问题
- 在并发模式下安全地处理状态更新
如果 reducer 有副作用,这些场景都会出问题。
action 要表达业务动作
好的 action 描述的是"用户做了什么",而不是"我要调用哪个 setter"。
php
// ❌ 不好的 action:描述的是技术操作
dispatch({ type: 'setStatus', value: 'submitting' });
dispatch({ type: 'setError', value: null });
// ✅ 好的 action:描述的是业务动作
dispatch({ type: 'submit_started' });
dispatch({ type: 'submit_failed', error: err });
好的 action 让你一看就知道"发生了什么",而不是"改了哪个变量"。
Part 3: useReducer 实战
基本语法
useReducer 的用法和 useState 类似,但返回三个值:
scss
const [state, dispatch] = useReducer(reducer, initialState);
state:当前状态dispatch:触发更新的函数reducer:你写的处理函数initialState:初始状态
来看一个完整的例子:
php
import { useReducer } from 'react';
function tasksReducer(tasks, action) {
switch (action.type) {
case 'added':
return [...tasks, { id: action.id, text: action.text, done: false }];
case 'changed':
return tasks.map(task => (task.id === action.task.id ? action.task : task));
case 'deleted':
return tasks.filter(task => task.id !== action.id);
default:
throw Error('Unknown action: ' + action.type);
}
}
function TaskApp() {
const [tasks, dispatch] = useReducer(tasksReducer, initialTasks);
function handleAddTask(text) {
dispatch({ type: 'added', id: nextId++, text });
}
function handleChangeTask(task) {
dispatch({ type: 'changed', task });
}
function handleDeleteTask(taskId) {
dispatch({ type: 'deleted', id: taskId });
}
// ...
}
从 useState 迁移到 useReducer
迁移过程分三步:
第一步:把设置 state 的逻辑改写成 dispatch action
php
// 之前
function handleAddTask(text) {
setTasks([...tasks, { id: nextId++, text, done: false }]);
}
// 之后
function handleAddTask(text) {
dispatch({ type: 'added', id: nextId++, text });
}
第二步:编写 reducer 函数
把所有更新逻辑集中到一个地方:
python
function tasksReducer(tasks, action) {
switch (action.type) {
case 'added':
return [...tasks, { id: action.id, text: action.text, done: false }];
case 'changed':
return tasks.map(task => (task.id === action.task.id ? action.task : task));
case 'deleted':
return tasks.filter(task => task.id !== action.id);
default:
throw Error('Unknown action: ' + action.type);
}
}
第三步:在组件中用 useReducer
scss
const [tasks, dispatch] = useReducer(tasksReducer, initialTasks);
什么时候该用 useReducer
一个简单的判断标准:
| 场景 | 推荐 |
|---|---|
| 只有一两个简单状态 | useState |
| 状态逻辑复杂,操作很多 | useReducer |
| 多个事件处理函数修改同一个状态 | useReducer |
| 想更容易测试状态更新逻辑 | useReducer |
我的经验是:只要你开始频繁在不同函数里改同一份 state,就该警觉了。因为这往往意味着后面会越来越难维护。
不要为了"高级"而用 reducer。它的价值在于整理复杂逻辑,不在于替代所有
useState。
常见错误:reducer 里做副作用
php
// ❌ 错误:reducer 里发请求
function tasksReducer(tasks, action) {
switch (action.type) {
case 'added':
const response = await fetch('/api/tasks', { method: 'POST', body: action.text });
return [...tasks, response.json()];
// ...
}
}
// ✅ 正确:在事件处理函数或 Effect 里发请求
async function handleAddTask(text) {
const response = await fetch('/api/tasks', { method: 'POST', body: text });
const newTask = await response.json();
dispatch({ type: 'added', task: newTask });
}
Reducer 只负责计算新状态,副作用应该在外面做。
Part 4: Context 解决什么问题
prop drilling 的痛点
前面说过,prop drilling 会让中间组件被迫当"快递员"。
来看一个更具体的例子。假设你在做一个主题切换功能:
javascript
function App() {
const [theme, setTheme] = useState('light');
return (
<Layout theme={theme}>
<Header theme={theme} />
<Main theme={theme}>
<Sidebar theme={theme} />
<Content theme={theme}>
<Button theme={theme} />
</Content>
</Main>
</Layout>
);
}
Layout、Header、Main、Sidebar、Content 都不需要 theme,它们只是被迫传下去。如果以后要加一个 setTheme,又得多传一层。
Context 的三步曲
Context 提供了一种"跨中间层传值"的机制。分三步:
第一步:创建 context
javascript
import { createContext } from 'react';
export const ThemeContext = createContext('light');
createContext 的参数是默认值,当上层没有 provider 时使用。
第二步:提供 context
javascript
function App() {
const [theme, setTheme] = useState('light');
return (
<ThemeContext value={theme}>
<Layout />
</ThemeContext>
);
}
ThemeContext 的 value 属性就是你要传递的值。
第三步:消费 context
javascript
import { useContext } from 'react';
function Button() {
const theme = useContext(ThemeContext);
return (
<button className={theme === 'dark' ? 'btn-dark' : 'btn-light'}>
点击我
</button>
);
}
useContext(SomeContext) 会读取最近的 provider 提供的值。如果上层没有 provider,就使用 createContext 时传入的默认值。
对比一下
xml
// ❌ 没有 Context:层层传递
<Layout theme={theme}>
<Header theme={theme} />
<Main theme={theme}>
<Button theme={theme} />
</Main>
</Layout>
// ✅ 有 Context:直达目标
<ThemeContext value={theme}>
<Layout>
<Header />
<Main>
<Button />
</Main>
</Layout>
</ThemeContext>
中间组件不再需要关心 theme,它们变干净了。
什么时候该用 Context
Context 适合传递"周围环境"类的数据:
- 主题(theme)
- 当前用户(currentUser)
- 语言环境(locale)
- 路由信息(router)
- 某个功能域内多层组件都要读取的状态
什么时候不该急着用 Context
如果只是传一两层,props 往往更直接。
因为 props 的依赖关系是显式的,读代码时很容易看懂。而 Context 一旦用多了,数据来源会变得不够直观。
先问自己:这个值是真的"很多层都要用"吗?如果不是,就先用 props。
Part 5: Reducer + Context 组合模式
到这里,你已经学了两件事:
useReducer可以整理复杂的状态更新逻辑- Context 可以避免层层传递 props
把它们结合起来,就是 React 原生组织复杂状态的一种常见方式。
典型问题
假设你在做一个 TodoApp,状态在顶层:
scss
const [todos, dispatch] = useReducer(todosReducer, initialTodos);
接下来你会发现:
TodoList需要读todosAddTodo需要用dispatchTodoItem也需要用dispatch
如果继续用 props 层层往下传,组件树很快会变得臃肿。
分离 state 和 dispatch
常见做法是把 state 和 dispatch 放进两个不同的 context:
javascript
import { createContext } from 'react';
export const TodosContext = createContext(null);
export const TodosDispatchContext = createContext(null);
然后统一提供:
javascript
function TodoApp() {
const [todos, dispatch] = useReducer(todosReducer, initialTodos);
return (
<TodosContext value={todos}>
<TodosDispatchContext value={dispatch}>
<h1>待办事项</h1>
<AddTodo />
<TodoList />
</TodosDispatchContext>
</TodosContext>
);
}
深层组件直接读取:
javascript
function TodoList() {
const todos = useContext(TodosContext);
return (
<ul>
{todos.map(todo => (
<li key={todo.id}>{todo.text}</li>
))}
</ul>
);
}
function AddTodo() {
const dispatch = useContext(TodosDispatchContext);
function handleAdd(text) {
dispatch({ type: 'added', id: nextId++, text });
}
// ...
}
为什么分成两个 context
把读取状态和触发更新分开,有一个好处:
读代码时你会更容易分清:哪些组件只是消费数据,哪些组件还会发起更新。
scss
// 只读数据
function TodoList() {
const todos = useContext(TodosContext);
// ...
}
// 只触发更新
function AddTodo() {
const dispatch = useContext(TodosDispatchContext);
// ...
}
自定义 Hook 封装
为了让业务组件更干净,可以把 context 的读取封装成自定义 Hook:
scss
function useTodos() {
return useContext(TodosContext);
}
function useTodosDispatch() {
return useContext(TodosDispatchContext);
}
这样业务组件用起来更简洁:
javascript
function TodoList() {
const todos = useTodos();
return (
<ul>
{todos.map(todo => (
<li key={todo.id}>{todo.text}</li>
))}
</ul>
);
}
而且如果以后你想换实现方式(比如换成 Zustand),只需要改 Hook 内部,业务组件不用动。
完整的 TodoApp 案例
把上面的代码整合起来:
javascript
import { createContext, useContext, useReducer } from 'react';
// Context
const TodosContext = createContext(null);
const TodosDispatchContext = createContext(null);
// Reducer
function todosReducer(todos, action) {
switch (action.type) {
case 'added':
return [...todos, { id: action.id, text: action.text, done: false }];
case 'changed':
return todos.map(todo => (todo.id === action.todo.id ? action.todo : todo));
case 'deleted':
return todos.filter(todo => todo.id !== action.id);
default:
throw Error('Unknown action: ' + action.type);
}
}
// Provider 组件
function TodosProvider({ children }) {
const [todos, dispatch] = useReducer(todosReducer, initialTodos);
return (
<TodosContext value={todos}>
<TodosDispatchContext value={dispatch}>
{children}
</TodosDispatchContext>
</TodosContext>
);
}
// 自定义 Hook
function useTodos() {
return useContext(TodosContext);
}
function useTodosDispatch() {
return useContext(TodosDispatchContext);
}
// 业务组件
function TodoList() {
const todos = useTodos();
return (
<ul>
{todos.map(todo => (
<li key={todo.id}>{todo.text}</li>
))}
</ul>
);
}
function AddTodo() {
const dispatch = useTodosDispatch();
const [text, setText] = useState('');
function handleSubmit(e) {
e.preventDefault();
if (text.trim()) {
dispatch({ type: 'added', id: nextId++, text });
setText('');
}
}
return (
<form onSubmit={handleSubmit}>
<input value={text} onChange={e => setText(e.target.value)} />
<button type="submit">添加</button>
</form>
);
}
// App
export default function App() {
return (
<TodosProvider>
<h1>待办事项</h1>
<AddTodo />
<TodoList />
</TodosProvider>
);
}
这个模式把:
- 状态定义(reducer)
- 状态提供(context)
- 状态消费(useContext)
分开了。每个部分职责清晰,也更容易测试和维护。
Part 6: 什么时候不该用
简单组件不需要
如果你的组件只有一两个简单状态,useState 完全够用。
scss
// 这种情况不需要 useReducer
const [isOpen, setIsOpen] = useState(false);
const [count, setCount] = useState(0);
Reducer 的价值在于整理复杂逻辑。如果你的逻辑不复杂,用 reducer 反而增加了代码量。
和外部状态管理库的对比
当应用规模变大后,你可能会考虑引入外部状态管理库(如 Zustand、Redux、Jotai 等)。
| 方案 | 适合场景 | 优点 | 缺点 |
|---|---|---|---|
useState |
简单、局部状态 | 零配置,最简单 | 逻辑分散后难维护 |
useReducer |
单组件复杂状态 | 逻辑集中,纯函数 | 只在单组件内有效 |
Reducer + Context |
跨组件共享状态 | 原生方案,零依赖 | 性能可能有问题 |
| Zustand / Jotai | 中大型应用 | 简洁,性能好 | 需要引入依赖 |
| Redux | 大型团队协作 | 生态完善,可预测 | 样板代码多,学习曲线陡 |
个人建议:先用 Reducer + Context,等你真正感觉到它的局限(比如性能问题、代码量问题),再考虑外部库。不要过早优化。
选型建议
简单判断:
- 只是局部状态 :
useState - 单组件复杂逻辑 :
useReducer - 跨组件共享,规模不大:Reducer + Context
- 跨组件共享,规模较大:考虑 Zustand / Jotai
- 大型团队,需要严格规范:考虑 Redux
实际项目中,大多数情况用 useState + props 就够了。当你真正需要时,Reducer + Context 是一个很好的起点。
总结
这篇文章讲了两个核心概念:
- Reducer:把状态更新逻辑集中到一个纯函数里,让"发生了什么"和"怎么更新"分开
- Context:让数据"跳过"中间层直达需要它的组件,避免 prop drilling
把它们组合起来,就是 React 原生组织复杂状态的一种常见模式:
- 用
useReducer管理状态和更新逻辑 - 用
createContext创建 context - 用 Provider 把 state 和 dispatch 传下去
- 用
useContext在深层组件里读取 - 用自定义 Hook 封装读取逻辑
但记住:这些工具是为复杂场景准备的 。如果你的组件很简单,useState + props 就是最好的选择。
相关资源
- React 官方文档 - 迁移状态逻辑至 Reducer 中
- React 官方文档 - 使用 Context 深层传递参数
- React 官方文档 - 使用 Reducer 和 Context 拓展你的应用
本文基于 React 官方文档"状态管理"章节整理。