状态管理
状态管理的目标是“让变化可预测”,不是把所有东西都塞进一个全局 `store`。
最好的状态架构通常是“在归属清晰的前提下尽量小”。先局部,需要共享再上提,只有在产品确实需要时才全局化。
选择标准
把这些标准当作清单。若无法用它们合理解释为什么要全局化,就把状态留在更靠近使用处的地方。
- 数据生命周期:仅当前组件/页面?还是跨页面持久?
- 更新频率:高频更新是否会放大渲染开销?
- 一致性:是否需要事务化更新、回放、持久化?
状态分类(你在管理哪种状态?)
很多团队过早上全局 store,根因是把不同类型的“状态”混在一起。先分类,解法往往自然浮现。
一旦你把状态类型命名清楚,工具选择也会随之清晰:UI 状态偏局部更新;服务端状态偏缓存/失效;URL 状态偏稳定与可分享。
- UI 状态:短暂、与视图强相关(`isOpen`、`selectedTab`、输入草稿)。
- 服务端状态:远程数据 + 缓存/失效/重试(列表、用户信息)。
- URL 状态:可分享/可书签(筛选、分页、排序)。
- 派生状态:可由其他 state/props 计算得到——能算出来就不要再存一份。
Context 的坑与更稳健的基线
`Context` 很适合跨层传递,但它不是“免费的全局 `store`”:一次更新可能触发大范围重渲染。
一个健康的默认做法是:保持 `Context` value 稳定;把更新控制在局部;当更新频率不同就拆分 provider。
- 避免把高频变化(鼠标位置、输入实时值)放进 Context。
- 按职责拆 Context:state/dispatch 分离,或按功能域切片。
- 默认局部;只有“确实需要共享”时再上提。
TypeScript
import { createContext, useContext, useReducer } from 'react';
type State = { count: number };
type Action = { type: 'inc' } | { type: 'dec' };
function reducer(state: State, action: Action): State {
switch (action.type) {
case 'inc':
return { count: state.count + 1 };
case 'dec':
return { count: state.count - 1 };
}
}
const StateContext = createContext<State | undefined>(undefined);
const DispatchContext = createContext<React.Dispatch<Action> | undefined>(undefined);
export function CounterProvider({ children }: { children: React.ReactNode }) {
const [state, dispatch] = useReducer(reducer, { count: 0 });
return (
<StateContext.Provider value={state}>
<DispatchContext.Provider value={dispatch}>{children}</DispatchContext.Provider>
</StateContext.Provider>
);
}
export function useCounterState() {
const s = useContext(StateContext);
if (!s) throw new Error('Missing CounterProvider');
return s;
}
export function useCounterDispatch() {
const d = useContext(DispatchContext);
if (!d) throw new Error('Missing CounterProvider');
return d;
}什么时候需要外部 store
外部 `store` 的价值在于“订阅模型”和“一致性”,不是“看起来更专业”。当你需要按需订阅或跨标签页持久时,它反而可能是最简单的解法。
- 跨路由/跨标签页持久(例如草稿、编辑器状态)。
- 消费方很多且需要“按需订阅”(避免“全站一起渲染”)。
- 复杂一致性需求:事务、撤销/重做、回放、乐观更新。
一个实用模式
当状态需要在一棵子树内共享时,Context + useReducer 往往是一个稳健的基线方案。
TypeScript
import { createContext, useContext, useReducer } from 'react';
type State = { count: number };
type Action = { type: 'inc' } | { type: 'dec' } | { type: 'reset' };
function reducer(state: State, action: Action): State {
switch (action.type) {
case 'inc':
return { count: state.count + 1 };
case 'dec':
return { count: state.count - 1 };
case 'reset':
return { count: 0 };
}
}
const CounterContext = createContext<
| { state: State; dispatch: React.Dispatch<Action> }
| undefined
>(undefined);
export function CounterProvider({ children }: { children: React.ReactNode }) {
const [state, dispatch] = useReducer(reducer, { count: 0 });
return (
<CounterContext.Provider value={{ state, dispatch }}>
{children}
</CounterContext.Provider>
);
}
export function useCounter() {
const ctx = useContext(CounterContext);
if (!ctx) throw new Error('Missing CounterProvider');
return ctx;
}上线检查清单
- 状态归属清晰:每个领域只有一个事实来源(`source of truth`)。
- 不重复存派生状态(需要时用计算 + `memo`)。
- `Context` 值稳定且范围可控;高频更新已隔离。
- 共享状态有持久化策略(如需要)以及清理/重置机制。
延伸阅读
- Learn:Context / useReducer