跳转到主要内容

状态管理

状态管理的目标是“让变化可预测”,不是把所有东西都塞进一个全局 `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` 值稳定且范围可控;高频更新已隔离。
  • 共享状态有持久化策略(如需要)以及清理/重置机制。

延伸阅读

状态管理 - Guides - React 文档 - React 文档