不止是状态管理:useReducer 与 useContext 的协作之道
从“跨层传参太痛、状态越来越乱、多个组件要协同更新”这些真实场景出发,讲清 useReducer 的可预测更新模型、Context 的作用域与性能特性,并给出一套可落地的组合范式:Reducer 驱动 + 分层 Context + 选择性订阅的优化策略
当你项目从“几个页面”长到“几十个组件”后,很多问题会一起涌上来:
- props 一路往下传,传到第 5 层你已经不知道这玩意儿从哪来的
- 多个组件要协同更新一份状态,改动点越来越多,逻辑越来越散
- 你不想上 Redux(太重),但也不想全靠 useState(太乱)
这时候最实用的一套中间解是:
useReducer 管更新规则,useContext 管共享范围。这篇我们把它讲成一套可落地的架构范式,并顺手把 Context 的性能坑讲透。
0. 先给结论:useReducer 解决“怎么改”,Context 解决“谁能用”
你可以把它们的职责分得很清楚:
useReducer:把状态变化收口到一个纯函数里(可预测、可测试)useContext:把这份状态(以及 dispatch)在组件树某个范围内共享
所以它们组合起来就是:
在某个 Provider 范围内,用 reducer 统一管理状态与更新。
1. 为什么复杂状态更适合 useReducer?因为它让变化可推理
1.1 useState 在复杂场景里的典型“失控”表现
当状态拆成一堆 useState:
- 更新逻辑分散在各个 handler/effect 里
- “A 更新会影响 B” 的因果关系很难追
- 很容易出现“先后顺序依赖”的 bug
useReducer 提供一种“状态机化”的表达:
function reducer(state, action) {
switch (action.type) {
case 'add':
return { ...state, items: [...state.items, action.payload] }
case 'remove':
return { ...state, items: state.items.filter((i) => i.id !== action.id) }
default:
return state
}
}
它的优势是:
- 更新入口统一:
dispatch(action) - 变化集中:只有 reducer 能决定新 state
- 纯函数易测试:输入 state + action => 输出 nextState
2. Context 到底解决什么?——跨层共享,但要明确“范围”
Context 常见用途:
- 主题 theme、国际化 i18n
- 用户信息 user、权限 auth
- 全局通知 toast、modal 管理
- 中型页面模块的共享状态(列表筛选、编辑器状态等)
关键点是:
Context 不是“全局”,它是“树上的一个范围”。
Provider 放在哪,谁能消费(useContext)就由树结构决定。
这也意味着一个好实践:
- 不要把 Provider 放得太高
- 只在需要共享的模块范围内提供
3. 组合范式:Reducer + Context 的最小可用模板(中型项目很好用)
3.1 创建 Context(分成 State 与 Dispatch 两个)
为什么要分两个?后面性能部分你会懂。
const StateContext = React.createContext(null)
const DispatchContext = React.createContext(null)
function AppProvider({ children }) {
const [state, dispatch] = React.useReducer(reducer, initialState)
return (
<DispatchContext.Provider value={dispatch}>
<StateContext.Provider value={state}>{children}</StateContext.Provider>
</DispatchContext.Provider>
)
}
function useAppState() {
const ctx = React.useContext(StateContext)
if (ctx === null)
throw new Error('useAppState must be used within AppProvider')
return ctx
}
function useAppDispatch() {
const ctx = React.useContext(DispatchContext)
if (ctx === null)
throw new Error('useAppDispatch must be used within AppProvider')
return ctx
}
使用:
function Toolbar() {
const dispatch = useAppDispatch();
return <button onClick={() => dispatch({ type: "add", payload: ... })}>Add</button>;
}
function List() {
const state = useAppState();
return state.items.map(...);
}
3.2 为什么推荐“State/Dispatch 分开”?
因为 dispatch 本身是稳定的(同一个函数引用),拆开后:
- 只需要 dispatch 的组件不会因为 state 变化而重渲染
- 能显著减少 Context 触发的无效更新
4. Context 的性能真相:Provider 的 value 一变,所有消费者都会重渲染
这是 Context 的核心特性(也是坑):
当 Provider 的
value引用变化时,所有useContext该 Context 的组件都会 re-render。
所以如果你把一个大对象 state 放进 Context:
- state 每次更新都会变引用(不可变更新)
- 所有消费 state 的组件都会重渲染
- 哪怕它们只用到了 state 的某个小字段
这不是 React “做得不好”,而是 Context 的设计决定的。
5. 三种常见优化策略(从易到难)
5.1 策略 1:拆 Context(最推荐,最简单)
把大 state 拆成多个更小的 context:
UserContextThemeContextTodoStateContextTodoDispatchContextFilterContext
原则:
让“变化频率高”的部分不要影响“变化频率低”的部分。
5.2 策略 2:Provider 下沉(缩小影响范围)
不要把 Provider 放到 App 根上,能放模块级就放模块级:
<Page>
<TodoProvider>
<TodoList />
<TodoToolbar />
</TodoProvider>
</Page>
这样 todo 更新不会影响整个应用。
5.3 策略 3:选择性消费(selector 思路)
Context 原生不支持 selector(即只订阅某个字段),但你可以通过:
- 拆 context(本质是 selector 的替代)
- 或引入专门库(如 use-context-selector 等)
- 或上 Zustand/Redux(它们天生支持 selector)
当你发现:
- context 拆到很多个仍不够
- 或页面很大、更新频繁、性能要求高
这时就该认真评估使用状态库了。
6. 一个常见误区:把“计算派生”也塞进 reducer
Reducer 应该做:
- 状态转移(state transition)
- 保持纯函数与可预测
不建议在 reducer 里做:
- 重度计算
- 非确定性逻辑(读取时间、随机数、外部 IO)
- 依赖环境的行为
派生值更适合:
- 渲染期间计算
useMemo- 或在 selector 层处理
7. 实战范式:用 reducer 管业务流程(loading/error/status)
例如一个典型请求流程,不要散在多个 useState 里:
const initialState = { status: 'idle', data: null, error: null }
function reducer(state, action) {
switch (action.type) {
case 'start':
return { status: 'loading', data: null, error: null }
case 'success':
return { status: 'success', data: action.data, error: null }
case 'error':
return { status: 'error', data: null, error: action.error }
default:
return state
}
}
组件只关心:
- 现在 status 是什么
- 根据 status 渲染不同 UI
- dispatch 触发状态转移
这样逻辑会非常稳定。
8. 什么时候这套方案就够了?什么时候该上状态库?
8.1 这套方案很适合
- 单页或单模块的中型复杂度
- 共享范围可控(模块级 Provider)
- 更新频率适中
- 团队想保持原生 React 简洁
8.2 考虑上 Zustand/Redux/Recoil 的信号
- 需要跨页面/跨路由共享大量状态
- 需要 selector、devtools、时间旅行、持久化等能力
- Context 拆到很多层仍然导致性能问题
- 业务状态多且更新频繁(实时协作、复杂编辑器等)
9. 本文小结:一套可落地的“中型状态管理”模板
你只需要记住这 5 点:
useReducer:统一更新入口,让变化可预测、可测试useContext:把 state/dispatch 在某个范围共享- Provider 范围要克制:能模块级就别全局
- State/Dispatch 拆 Context:降低无效重渲染
- 真正需要 selector 时,拆 context 到极限仍不够,再考虑状态库
留言讨论
Discussion
欢迎交流与反馈