类型安全的状态管理

56 minAdvanced2026/7/21

构建类型安全的状态管理系统,涵盖 Store 模式、Reducer/Action、选择器、中间件、异步流、不可变更新、有限状态机、原子状态与 Signal 响应式,并提供生产级最佳实践与案例研究。

类型安全的状态管理

学习目标

本节按 Bloom 认知层级组织学习目标,从基础记忆到高级创造逐层递进:

  • 记忆(Remember):能够复述状态管理的核心概念(Store、State、Action、Reducer、Selector、Middleware),列出至少 5 种主流状态管理方案(Redux、Zustand、Pinia、MobX、XState)的设计理念,说出 Flux/Redux 三大原则的内容。
  • 理解(Understand):能够解释单向数据流的动机,说明 reducer 纯函数约束的必要性,描述选择器记忆化(Memoization)的原理,对比 push-based 与 pull-based 响应式系统的差异。
  • 应用(Apply):能够在 TypeScript 项目中实现一个类型安全的 Store,编写 discriminated union 形式的 Action,使用 useSyncExternalStore 将 Store 接入 React,并通过中间件机制扩展日志、持久化、DevTools 能力。
  • 分析(Analyze):能够分析不同状态范式(全局 Store、原子状态、Signal、FSM)在性能、可测试性、可追溯性维度的权衡,识别状态 Normalization 的收益与边界,诊断不必要的重渲染根源。
  • 评估(Evaluate):能够评估在何种场景应引入 Redux Toolkit、Zustand、XState 或 Jotai,判断状态归属(局部 vs 全局、服务端 vs 客户端),并度量状态架构的可维护性指标(耦合度、变更影响面、测试覆盖)。
  • 创造(Create):能够为大型应用(50+ 路由、100+ 组件)设计端到端类型安全的状态架构,包含领域切片划分、派生状态层、效果编排、持久化策略、时间旅行调试、SSR 水合、测试金字塔,并预留扩展点支持未来微前端或联邦化。

历史动机与背景

前端状态的混沌时代

2010 年以前,前端状态管理处于”混沌期”。jQuery 时代的典型模式是:DOM 即状态,事件处理器直接操作 DOM,数据散落在各个闭包与全局变量中。这种模式带来的问题包括:

  • 状态分散:同一份数据可能存在于多个 DOM 节点、变量、localStorage 中,难以追踪。
  • 同步困难:A 处修改后,B 处的依赖难以自动更新,导致 UI 不一致。
  • 调试痛苦:Bug 复现依赖具体操作序列,缺乏”状态快照”概念。
  • 测试几乎不可能:状态与 UI 强耦合,单元测试无从下手。

MVC 与双向绑定的尝试

Backbone.js(2010)引入了 MVC 模式,Knockout.js 与 Angular.js(2010-2012)推广了双向数据绑定(MVVM)。这些方案缓解了部分问题,但引入了新困境:

  • 双向绑定导致数据流难以追踪:A 改 B,B 改 C,C 又改 A,形成环路。
  • Watchers 性能爆炸:Angular 1.x 的脏检查机制在 2000+ watcher 时明显卡顿。
  • 大型应用难以维护:Facebook 的广告创建界面在 2014 年已达 5000+ 行 jQuery 代码,Bug 修复成本指数级上升。

Flux 的诞生

2014 年,Facebook 提出 Flux 架构,核心理念是单向数据流

Action → Dispatcher → Store → View
          ↑________________________|

Flux 的关键约束:

  1. Action 是唯一的状态变更入口,必须显式 dispatch。
  2. Store 是状态的唯一持有者,不可直接修改,只能通过注册的回调响应 Action。
  3. View 只能读取 Store,变更必须通过 Action 发起。

这一架构解决了双向绑定的环路问题,但 Flux 原始实现复杂、Store 之间依赖管理困难。社区很快推出了简化版本。

Redux 的简化与普及

2015 年,Dan Abramov 发布 Redux,将 Flux 简化为三条原则:

  1. 单一数据源:整个应用的状态存储在一个对象树中。
  2. 状态只读:改变状态的唯一方式是 dispatch 一个 action。
  3. 纯函数变更:Reducer 是纯函数,接收旧状态与 action,返回新状态。

Redux 的精妙之处在于:** reducer 的纯函数约束使状态变更可预测、可序列化、可回放**。配合时间旅行调试,开发者可以”倒带”查看任意时刻的状态。

然而,Redux 的样板代码(boilerplate)问题突出——一个简单的计数器需要定义 action type、action creator、reducer 三层。这催生了 Redux Toolkit(2019)、Zustand(2019)等简化方案。

响应式范式的演进

与 Redux 的”拉取式”(pull-based)数据流并行发展的是”推送式”(push-based)的响应式范式:

  • MobX(2015):基于观察者模式,自动追踪依赖,状态变更自动传播。语法贴近 OOP,但隐式依赖追踪使数据流不透明。
  • RxJS(2015):函数式响应编程,将一切视为流。强大但学习曲线陡峭,Redux Observable 是其在状态管理领域的应用。
  • Solid Signals(2018-2021):细粒度响应式,无需 VDOM diff,性能接近原生。
  • Vue 3 Reactivity(2020):基于 Proxy 的响应式系统,配合 Composition API。
  • Angular Signals(2023):Angular 引入 Signal 概念,逐步替代 zone.js 的脏检查。
  • Svelte Runes(2024):Svelte 5 的细粒度响应式语法。
  • React 19 useHook(2024-2025):React 拥抱编译期优化,useSyncExternalStore 成为外部 Store 接入的标准。

状态范式的多元化(2020-2025)

2020 年后,状态管理进入”范式多元化”时代:

  • 原子状态(Atomic State)Recoil(2020)、Jotai(2020)、Vejrox(2022)。将状态拆分为最小粒度的原子,避免单一 Store 的性能瓶颈。
  • 代理状态(Proxy State)Valtio(2020)。基于 Proxy 的可变状态 API,运行时自动生成不可变快照。
  • 有限状态机(FSM)XState(2018-2024)。将状态建模为状态机,强调”非法状态不可表达”。
  • 服务端状态分离React Query(2019)、SWR(2019)。将服务端缓存状态从客户端 UI 状态中分离,各司其职。
  • URL 状态nuqs(2023)。将状态同步到 URL,支持分享与刷新。
  • Zustand 极简主义Zustand(2019)。以 1KB 体积提供 Redux 的核心能力,API 极简,TypeScript 友好。

截至 2025 年,主流社区共识是:没有银弹,按场景选择。客户端 UI 状态用 Zustand/Jotai,服务端状态用 React Query,复杂业务流程用 XState,大型团队协作用 Redux Toolkit。

TypeScript 的角色

TypeScript 在状态管理中扮演关键角色:

  • 类型安全的状态形状:编译期捕获拼写错误、字段类型错误。
  • Discriminated Union 的 Actionswitch (action.type) 模式下,编译器自动缩窄 payload 类型。
  • 派生类型的 Selector:选择器返回类型由输入类型推导,无需手动标注。
  • 中间件的类型推导thunksaga 等中间件的参数类型自动推导。
  • DevTools 的类型提示:Action 历史与状态快照在 DevTools 中有类型信息。

然而,TypeScript 的类型系统在状态管理中也面临挑战:深层嵌套的不可变更新类型、跨切片的依赖推导、运行时校验与编译期类型的一致性。本节将系统性地解决这些挑战。

形式化定义

状态与 Store 的形式化

S\mathcal{S} 为应用所有可能状态的集合,sSs \in \mathcal{S} 为某一时刻的状态。Store 是一个三元组:

Store=s,getState,setState\text{Store} = \langle s, \text{getState}, \text{setState} \rangle

其中:

  • sSs \in \mathcal{S}:当前状态(私有,外部不可直接访问)。
  • getState:1S\text{getState} : \mathbb{1} \to \mathcal{S}:返回当前状态的快照。
  • setState:(SS)Spartial1\text{setState} : (\mathcal{S} \to \mathcal{S}) \cup \mathcal{S}_{\text{partial}} \to \mathbb{1}:更新状态。

关键约束:状态不可变。即 setState\text{setState} 不修改 ss,而是创建新的 ss' 并替换:

setState(f)    snew=f(sold),sold 不变\text{setState}(f) \implies s_{\text{new}} = f(s_{\text{old}}), \quad s_{\text{old}} \text{ 不变}

Action 与 Reducer 的形式化

Action 是描述状态变更意图的对象,形式化为 tagged union:

Action=i=1n{type:Ti,payload:Pi}\text{Action} = \bigsqcup_{i=1}^{n} \{ \text{type}: T_i, \text{payload}: P_i \}

其中 TiT_i 是字面量类型(如 'increment''setUser'),PiP_i 是对应的 payload 类型。Discriminated Union 保证 switch (action.type) 时编译器能缩窄类型。

Reducer 是纯函数:

reducer:S×ActionS\text{reducer} : \mathcal{S} \times \text{Action} \to \mathcal{S}

必须满足:

  1. 纯函数reducer(s,a)\text{reducer}(s, a) 不产生副作用,相同输入相同输出。
  2. 不可变:返回新状态,不修改 ss
  3. 默认返回:未知 action 返回原状态:reducer(s,{type:unknown})=s\text{reducer}(s, \{ \text{type}: \text{unknown} \}) = s

单向数据流的形式化

Redux 的单向数据流可形式化为状态转换系统:

st+1=reducer(st,at)s_{t+1} = \text{reducer}(s_t, a_t)

其中 sts_ttt 时刻状态,ata_ttt 时刻 dispatch 的 action。整个状态历史是一个序列:

H=[s0,s1,s2,,sn]\mathcal{H} = [s_0, s_1, s_2, \dots, s_n]

给定初始状态 s0s_0 与 action 序列 [a0,a1,,an1][a_0, a_1, \dots, a_{n-1}],可以完整重建任意时刻的状态。这是时间旅行调试的数学基础:

st=reducer(t)(s0,[a0,,at1])=reducer(reducer(t1)(s0,),at1)s_t = \text{reducer}^{(t)}(s_0, [a_0, \dots, a_{t-1}]) = \text{reducer}(\text{reducer}^{(t-1)}(s_0, \dots), a_{t-1})

Selector 与记忆化的形式化

Selector 是从状态派生值的纯函数:

selector:SV\text{selector} : \mathcal{S} \to \mathcal{V}

其中 V\mathcal{V} 是派生值的类型。记忆化(Memoization)通过缓存上次的输入与输出避免重复计算:

memoizedSelector(s)={cache.valueif s=cache.inputf(s)(并更新 cache)otherwise\text{memoizedSelector}(s) = \begin{cases} \text{cache.value} & \text{if } s = \text{cache.input} \\ f(s) \text{(并更新 cache)} & \text{otherwise} \end{cases}

更精细的策略是输入相等性检查而非引用相等:

reselect(s)={cache.valueif isEqual(s,cache.input)f(s)otherwise\text{reselect}(s) = \begin{cases} \text{cache.value} & \text{if } \text{isEqual}(s, \text{cache.input}) \\ f(s) & \text{otherwise} \end{cases}

reselect 库的核心即此。默认 isEqual===,可替换为 shallowEqualdeepEqual

中间件的形式化

中间件是”包裹” dispatch 的高阶函数,形式化为洋葱模型:

dispatchfinal=mnmn1m1dispatchbase\text{dispatch}_{\text{final}} = m_n \circ m_{n-1} \circ \dots \circ m_1 \circ \text{dispatch}_{\text{base}}

每个中间件 mim_i 接收 next(下一个 dispatch)与 action,可决定是否调用 next(action)、修改 action、或完全拦截:

mi:next×action1m_i : \text{next} \times \text{action} \to \mathbb{1}

常见中间件职责:

  • logger:记录 action 与状态变化。
  • thunk:允许 action 是函数,支持异步逻辑。
  • saga:基于 Generator 的复杂异步流。
  • persist:将状态持久化到 localStorage/IndexedDB。
  • devtools:与 Redux DevTools 通信。

有限状态机的形式化

有限状态机(FSM)是五元组:

M=Q,Σ,δ,q0,FM = \langle Q, \Sigma, \delta, q_0, F \rangle

其中:

  • QQ:有限状态集。
  • Σ\Sigma:有限输入集(事件)。
  • δ:Q×ΣQ\delta : Q \times \Sigma \to Q:状态转移函数。
  • q0Qq_0 \in Q:初始状态。
  • FQF \subseteq Q:终止状态集(前端场景通常为空)。

FSM 的核心价值是非法状态不可表达。例如,一个按钮的”加载中”状态不应与”禁用”状态同时存在——FSM 通过显式建模 idle | loading | success | error 四态,避免组合爆炸。

XState 将 FSM 扩展为状态图(Statechart),支持层次化状态、并行状态、守卫条件、副作用,由 Harel Statecharts 理论支撑。

理论推导

推导一:为何 Reducer 必须是纯函数

Redux 的核心约束是 reducer 纯函数。这一约束的必要性来自三个层面:

1. 可预测性

设 reducer 为 r:S×ActionSr : \mathcal{S} \times \text{Action} \to \mathcal{S}。若 rr 是纯函数,则:

s,a.  r(s,a) 的结果唯一确定\forall s, a. \; r(s, a) \text{ 的结果唯一确定}

这意味着给定相同状态与 action,结果必然相同。开发者无需考虑”这次调用会不会因为副作用产生不同结果”。

2. 可序列化

纯函数约束使状态历史可序列化为 [s0,a0,a1,,an][s_0, a_0, a_1, \dots, a_n]。这一序列可:

  • 保存到文件,供后续回放。
  • 通过网络传输,实现协作编辑。
  • 用于自动化测试,只需记录输入序列即可复现任意 Bug。

若 reducer 有副作用(如发起网络请求、修改全局变量),则序列化丢失了关键信息,回放结果会偏离。

3. 时间旅行可行性

时间旅行要求”从 s0s_0 任意跳转到 sts_t“。若 reducer 纯,则:

st=r(r(r(s0,a0),a1),,at1)s_t = r(r(\dots r(s_0, a_0), a_1), \dots, a_{t-1})

这一计算无副作用,可任意重复执行。若 reducer 不纯,则跳转可能触发副作用(如重复发起请求),导致应用状态混乱。

反例:thunk 中间件为何”打破”纯函数约束

thunk 允许 dispatch 一个函数而非 action 对象,函数内部可包含副作用。这看似违反纯函数原则,实则不然:

  • thunk 是中间件层面的能力,reducer 仍是纯函数。
  • thunk 函数的副作用发生在 dispatch 之前,reducer 接收的仍是纯 action。
  • 副作用与状态变更分离,是 Flux 标准模式(Flux Standard Action)的体现。

推导二:Discriminated Union 的类型缩窄

TypeScript 的 discriminated union 配合 switch 语句实现类型缩窄。形式化地,设:

Action={type:’A’,payload:PA}    {type:’B’,payload:PB}\text{Action} = \{ \text{type}: \text{'A'}, \text{payload}: P_A \} \;\sqcup\; \{ \text{type}: \text{'B'}, \text{payload}: P_B \}

action.type 被匹配为 'A' 时,TypeScript 编译器通过控制流分析将 action 缩窄为 {type:’A’,payload:PA}\{ \text{type}: \text{'A'}, \text{payload}: P_A \},从而访问 action.payload 时类型为 PAP_A

这一缩窄的可靠性基于:

  1. 字面量类型的不可变性type: 'A' 是字面量类型,运行时值严格等于 'A'
  2. switch 的穷尽性检查:若未覆盖所有 union 成员,never 类型断言会报错。
  3. 编译期与运行时的一致性:字面量类型在编译后被擦除,但运行时值仍为字符串,switch 判断有效。

工程实践中,常用辅助类型确保穷尽性:

function reducer(state: State, action: Action): State {
  switch (action.type) {
    case 'A': return handleA(state, action.payload);  // payload: P_A
    case 'B': return handleB(state, action.payload);  // payload: P_B
    default: {
      const _: never = action;  // 穷尽性检查
      return state;
    }
  }
}

若未来新增 case 'C' 但忘记处理,default 分支的 never 赋值会编译报错,强制开发者补全。

推导三:不可变更新的引用相等性

React 的 useSyncExternalStoreuseSelector 通过引用相等性判断状态是否变化。这要求不可变更新的返回值与原状态引用不同:

setState(f)    snewsold(引用层面)\text{setState}(f) \implies s_{\text{new}} \neq s_{\text{old}} \quad \text{(引用层面)}

但”引用不同”不等于”内容不同”。若 f 实际上返回了与原状态等价的对象(但新引用),会触发不必要的重渲染。反之,若 f 直接修改原状态(mutation),引用不变,React 不会感知变化,UI 不更新——这是最常见的 React Bug 之一。

不可变更新的标准模式是浅拷贝 + 递归:

update(s,path,v)={vif path 为空{s,[key]:update(s[key],rest,v)}otherwise\text{update}(s, \text{path}, v) = \begin{cases} v & \text{if path 为空} \\ \{ \dots s, [\text{key}]: \text{update}(s[\text{key}], \text{rest}, v) \} & \text{otherwise} \end{cases}

即沿路径创建新对象,未修改的分支保持原引用(结构共享)。这一策略的时间复杂度为 O(depth)O(\text{depth}),空间复杂度也为 O(depth)O(\text{depth}),远优于深拷贝。

immer 库通过 Proxy 实现”可变 API 写法,不可变结果”:

produce(state, draft => {
  draft.user.profile.age += 1;  // 看似 mutation
});
// 返回新对象,原 state 不变

推导四:原子状态的依赖图

原子状态(Recoil/Jotai)将状态拆分为最小粒度,通过依赖图自动传播更新。形式化地,原子集合 A={a1,a2,,an}\mathcal{A} = \{ a_1, a_2, \dots, a_n \},每个原子 aia_i 有值 viv_i。派生原子 djd_j 依赖若干基础原子:

dj=fj(vi1,vi2,,vik)d_j = f_j(v_{i_1}, v_{i_2}, \dots, v_{i_k})

vi1v_{i_1} 变化时,仅 djd_j 及其下游依赖被重新计算,其他原子不受影响。这一细粒度更新的复杂度为:

O(依赖图下游)而非O(整个 Store)O(|\text{依赖图下游}|) \quad \text{而非} \quad O(|\text{整个 Store}|)

对于大型应用,这一差异可达数量级。Redux 的 useSelector 虽然也支持细粒度订阅,但每次 dispatch 都会触发所有 selector 的运行(即使结果未变),而原子状态仅在依赖变化时才计算。

推导五:Signal 的推拉混合

Signal(Solid、Vue 3、Angular)采用”推拉混合”策略:

  • 写入时推送signal.set(v) 通知所有依赖该 signal 的 effect。
  • 读取时拉取:effect 执行时读取 signal 当前值,自动建立依赖关系。

这一策略通过依赖追踪实现:

effect()    追踪读取的 signals    建立 signaleffect 的边\text{effect}() \implies \text{追踪读取的 signals} \implies \text{建立 } \text{signal} \to \text{effect} \text{ 的边}

signal.set(v) 时,沿依赖图反向查找所有 effect,标记为”脏”。下次渲染时,仅脏 effect 重新执行。

与 React 的 VDOM diff 相比,Signal 的优势是无需 diff——精确知道哪些 effect 依赖哪些 signal,直接执行。代价是依赖追踪的运行时开销隐式依赖的调试难度

复杂度分析

状态管理操作的复杂度:

  • getStateO(1)O(1),直接返回引用。
  • setState(浅合并)O(top-level keys)O(|\text{top-level keys}|),创建新对象。
  • setState(immer)O(path depth)O(\text{path depth}),结构共享。
  • selector(无记忆化)O(compute cost)O(\text{compute cost}),每次调用都计算。
  • selector(reselect)O(equality check)+O(compute if changed)O(\text{equality check}) + O(\text{compute if changed})
  • dispatch(无中间件)O(reducer cost)+O(subscribers)O(\text{reducer cost}) + O(|\text{subscribers}|)
  • dispatch(有中间件):上述 + O(middleware)O(|\text{middleware}|)
  • 时间旅行跳转O(t)O(t),从头回放或从快照恢复。

对于 1000 个订阅者的大型应用,每次 dispatch 的通知成本为 O(1000)O(1000)。若每个订阅者的 selector 返回引用相同的值,则实际重渲染数为 0(被 useSelector 的相等性检查过滤)。这是 Redux 性能优化的关键。

代码示例

示例 1:类型安全的 Store 从零实现

// store.ts - 类型安全的 Store 实现
// 基于 useSyncExternalStore 设计,提供 React 友好的订阅 API

/**
 * Store 接口定义
 * @template S - 状态类型,必须是对象
 */
interface Store<S extends object> {
  /** 获取当前状态快照 */
  getState(): S;
  /** 更新状态,支持部分对象或更新函数 */
  setState(partial: Partial<S> | ((state: S) => Partial<S>)): void;
  /** 订阅状态变化,返回取消订阅函数 */
  subscribe(listener: () => void): () => void;
}

/**
 * 创建类型安全的 Store
 * @param initialState - 初始状态
 * @returns Store 实例
 */
function createStore<S extends object>(initialState: S): Store<S> {
  // 内部状态,外部不可直接访问
  let state = { ...initialState };
  // 订阅者集合,使用 Set 保证唯一性
  const listeners = new Set<() => void>();

  return {
    getState: () => state,
    setState(partial) {
      // 支持函数式更新,便于访问当前状态
      const update = typeof partial === 'function' ? partial(state) : partial;
      // 浅合并创建新状态(不可变更新)
      state = { ...state, ...update };
      // 通知所有订阅者
      listeners.forEach((listener) => listener());
    },
    subscribe(listener) {
      listeners.add(listener);
      // 返回取消订阅函数,符合 React useSyncExternalStore 的契约
      return () => listeners.delete(listener);
    },
  };
}

// 使用示例:定义应用状态类型
interface AppState {
  count: number;
  user: { id: string; name: string } | null;
  theme: 'light' | 'dark';
  notifications: ReadonlyArray<{ id: string; message: string; read: boolean }>;
}

// 创建 Store,初始状态必须满足 AppState
const store = createStore<AppState>({
  count: 0,
  user: null,
  theme: 'light',
  notifications: [],
});

// 类型安全的读取:编译器知道 count 是 number
const currentCount = store.getState().count;

// 类型安全的更新:传入的 partial 必须是 Partial<AppState>
store.setState({ count: 1 });
store.setState((state) => ({ count: state.count + 1 }));

// 错误示例会被编译器拒绝
// store.setState({ count: 'one' });  // Error: string 不是 number
// store.setState({ unknown: 1 });    // Error: unknown 不在 AppState 中

示例 2:React 集成(useSyncExternalStore)

// useStore.ts - 将 Store 接入 React 18+
import { useSyncExternalStore } from 'react';

/**
 * 订阅整个 Store 状态的 Hook
 * 注意:每次状态变化都会触发重渲染,慎用于大型状态树
 * @param store - Store 实例
 * @returns 当前状态
 */
function useStore<S extends object>(store: Store<S>): S {
  return useSyncExternalStore(
    store.subscribe,
    store.getState,
    store.getState,  // SSR 快照
  );
}

/**
 * 订阅 Store 的派生值(带选择器与相等性检查)
 * @param store - Store 实例
 * @param selector - 从状态派生值的函数
 * @param isEqual - 相等性检查函数,默认引用相等
 */
function useStoreSelector<S extends object, T>(
  store: Store<S>,
  selector: (state: S) => T,
  isEqual: (a: T, b: T) => boolean = Object.is,
): T {
  return useSyncExternalStoreWithSelector(
    store.subscribe,
    store.getState,
    store.getState,
    selector,
    isEqual,
  );
}

// 使用示例
import { useSyncExternalStoreWithSelector } from 'use-sync-external-store/with-selector';

function Counter() {
  // 仅订阅 count,user/theme 变化不触发重渲染
  const count = useStoreSelector(store, (state) => state.count);
  return <div>Count: {count}</div>;
}

function UserProfile() {
  // 仅订阅 user.name,其他字段变化不触发
  const userName = useStoreSelector(
    store,
    (state) => state.user?.name ?? '未登录',
  );
  return <div>Hello, {userName}</div>;
}

// 浅比较优化:返回新对象但内容相同时不重渲染
function NotificationList() {
  const unreadIds = useStoreSelector(
    store,
    (state) => state.notifications.filter((n) => !n.read).map((n) => n.id),
    // 浅比较数组内容
    (a, b) => a.length === b.length && a.every((v, i) => v === b[i]),
  );
  return <ul>{unreadIds.map((id) => <li key={id}>{id}</li>)}</ul>;
}

示例 3:Discriminated Union 的 Action 与 Reducer

// actions.ts - 类型安全的 Action 定义
// 使用 discriminated union 确保 switch 语句的类型缩窄

/**
 * 所有 Action 的联合类型
 * 每个 Action 有唯一的 type 字面量与对应的 payload
 */
type AppAction =
  | { type: 'INCREMENT'; payload: { step: number } }
  | { type: 'DECREMENT'; payload: { step: number } }
  | { type: 'SET_USER'; payload: { id: string; name: string } }
  | { type: 'CLEAR_USER' }  // 无 payload 的 Action
  | { type: 'SET_THEME'; payload: { theme: 'light' | 'dark' } }
  | { type: 'ADD_NOTIFICATION'; payload: { id: string; message: string } }
  | { type: 'MARK_NOTIFICATION_READ'; payload: { id: string } };

/**
 * Action 创建器:提供类型安全的工厂函数
 * 好处:调用处无需手写 type 字符串,避免拼写错误
 */
const actions = {
  increment: (step: number): AppAction => ({ type: 'INCREMENT', payload: { step } }),
  decrement: (step: number): AppAction => ({ type: 'DECREMENT', payload: { step } }),
  setUser: (id: string, name: string): AppAction => ({ type: 'SET_USER', payload: { id, name } }),
  clearUser: (): AppAction => ({ type: 'CLEAR_USER' }),
  setTheme: (theme: 'light' | 'dark'): AppAction => ({ type: 'SET_THEME', payload: { theme } }),
  addNotification: (message: string): AppAction => ({
    type: 'ADD_NOTIFICATION',
    payload: { id: crypto.randomUUID(), message },
  }),
  markNotificationRead: (id: string): AppAction => ({
    type: 'MARK_NOTIFICATION_READ',
    payload: { id },
  }),
} as const;

// reducer.ts - 纯函数 Reducer
import type { AppState } from './store';

/**
 * 应用主 Reducer
 * 纯函数:相同输入相同输出,无副作用,不修改原状态
 * @param state - 当前状态
 * @param action - 派发的 Action
 * @returns 新状态(不可变更新)
 */
function appReducer(state: AppState, action: AppAction): AppState {
  switch (action.type) {
    case 'INCREMENT':
      // 返回新对象,count 字段更新
      return { ...state, count: state.count + action.payload.step };

    case 'DECREMENT':
      return { ...state, count: state.count - action.payload.step };

    case 'SET_USER':
      return { ...state, user: { id: action.payload.id, name: action.payload.name } };

    case 'CLEAR_USER':
      return { ...state, user: null };

    case 'SET_THEME':
      return { ...state, theme: action.payload.theme };

    case 'ADD_NOTIFICATION':
      // 数组也是不可变更新:创建新数组而非 push
      return {
        ...state,
        notifications: [...state.notifications, action.payload],
      };

    case 'MARK_NOTIFICATION_READ':
      return {
        ...state,
        notifications: state.notifications.map((n) =>
          n.id === action.payload.id ? { ...n, read: true } : n
        ),
      };

    default: {
      // 穷尽性检查:若新增 Action 未处理,此处编译报错
      const _exhaustive: never = action;
      return state;
    }
  }
}

// 使用
store.setState((state) => appReducer(state, actions.increment(5)));
store.setState((state) => appReducer(state, actions.setUser('1', 'Alice')));

示例 4:记忆化 Selector(reselect 模式)

// selectors.ts - 类型安全的记忆化选择器
// 借鉴 reselect 库的设计,支持输入选择器组合与输出记忆化

/**
 * 创建记忆化选择器
 * @param inputs - 输入选择器数组,从状态派生若干值
 * @param resultFn - 结果函数,根据输入计算最终值
 * @param isEqual - 输入比较函数,默认引用相等
 */
function createSelector<S, Inputs extends ReadonlyArray<unknown>, Result>(
  inputs: { [K in keyof Inputs]: (state: S) => Inputs[K] },
  resultFn: (...args: Inputs) => Result,
  isEqual: (a: Inputs, b: Inputs) => boolean = (a, b) =>
    a.length === b.length && a.every((v, i) => Object.is(v, b[i])),
): (state: S) => Result {
  let lastInputs: Inputs | null = null;
  let lastResult: Result;

  return (state: S) => {
    // 计算当前输入
    const currentInputs = inputs.map((fn) => fn(state)) as Inputs;

    // 若输入未变,返回缓存结果
    if (lastInputs && isEqual(lastInputs, currentInputs)) {
      return lastResult;
    }

    // 输入变化,重新计算
    lastInputs = currentInputs;
    lastResult = resultFn(...currentInputs);
    return lastResult;
  };
}

// 使用示例
import type { AppState } from './store';

// 基础选择器:直接从状态取值
const selectCount = (state: AppState) => state.count;
const selectUser = (state: AppState) => state.user;
const selectNotifications = (state: AppState) => state.notifications;

// 派生选择器:组合多个基础选择器
const selectUserGreeting = createSelector(
  [selectCount, selectUser],
  (count, user) => {
    // 此处 count 与 user 都有精确类型
    if (!user) return `访客,计数 ${count}`;
    return `${user.name},计数 ${count}`;
  },
);

// 复杂派生:未读通知数
const selectUnreadCount = createSelector(
  [selectNotifications],
  (notifications) => notifications.filter((n) => !n.read).length,
);

// 链式组合:未读通知的消息列表
const selectUnreadMessages = createSelector(
  [selectNotifications],
  (notifications) => notifications.filter((n) => !n.read).map((n) => n.message),
);

// 使用:在组件中订阅
const greeting = selectUserGreeting(store.getState());  // string
const unreadCount = selectUnreadCount(store.getState());  // number

示例 5:中间件机制

// middleware.ts - 中间件实现
// 中间件是"包裹 dispatch 的高阶函数",支持日志、持久化、异步等横切关注点

/**
 * 中间件类型定义
 * @template S - 状态类型
 * @template A - Action 类型,默认为任意 Action
 */
type Middleware<S, A extends { type: string } = { type: string }> = (
  api: { getState: () => S; dispatch: (action: A) => void },
) => (next: (action: A) => void) => (action: A) => void;

/**
 * 日志中间件:记录每次 dispatch 的 action 与状态变化
 */
const loggerMiddleware: Middleware<AppState, AppAction> =
  ({ getState }) =>
  (next) =>
  (action) => {
    console.group(`Action: ${action.type}`);
    console.log('Payload:', action.payload);
    console.log('State before:', getState());
    const result = next(action);  // 调用下一个中间件或最终 dispatch
    console.log('State after:', getState());
    console.groupEnd();
    return result;
  };

/**
 * 持久化中间件:将状态保存到 localStorage
 * @param key - 存储键名
 * @param paths - 需持久化的状态路径(避免存储敏感数据)
 */
function createPersistMiddleware<S extends object>(
  key: string,
  paths: ReadonlyArray<keyof S>,
): Middleware<S> {
  return ({ getState }) =>
    (next) =>
    (action) => {
      const result = next(action);
      // dispatch 后保存指定路径
      const state = getState();
      const persistData: Partial<S> = {};
      paths.forEach((path) => {
        persistData[path] = state[path];
      });
      try {
        localStorage.setItem(key, JSON.stringify(persistData));
      } catch (e) {
        console.error('[persist] 保存失败:', e);
      }
      return result;
    };
}

/**
 * thunk 中间件:允许 dispatch 函数,支持异步逻辑
 * 函数接收 getState 与 dispatch,可执行副作用
 */
type Thunk<S, A extends { type: string }> = (
  dispatch: (action: A | Thunk<S, A>) => void,
  getState: () => S,
) => void | Promise<void>;

const thunkMiddleware = <S, A extends { type: string }>(): Middleware<S, A | Thunk<S, A>> =>
  ({ getState, dispatch }) =>
  (next) =>
  (action) => {
    // 若 action 是函数,调用它(thunk 模式)
    if (typeof action === 'function') {
      return action(dispatch, getState);
    }
    // 普通对象 action,交给下一个中间件
    return next(action);
  };

/**
 * 应用中间件链:类似 Koa 的洋葱模型
 */
function applyMiddleware<S extends object, A extends { type: string }>(
  createStore: (initialState: S) => Store<S>,
  middlewares: ReadonlyArray<Middleware<S, A>>,
): (initialState: S) => Store<S> & { dispatch: (action: A) => void } {
  return (initialState) => {
    const store = createStore(initialState);

    // 增强 dispatch:从右到左组合中间件
    const api = {
      getState: store.getState,
      dispatch: (action: A) => enhancedDispatch(action),
    };

    const chain = middlewares.map((mw) => mw(api));
    // next 链:最后一个 next 是原始 setState
    const enhancedDispatch = chain.reduceRight(
      (next, mw) => mw(next),
      (action: A) => {
        store.setState((state) => appReducer(state, action));
      },
    );

    return {
      ...store,
      dispatch: enhancedDispatch,
    };
  };
}

// 使用
const enhancedCreateStore = applyMiddleware(createStore, [
  thunkMiddleware(),
  loggerMiddleware,
  createPersistMiddleware('app-state', ['theme', 'user']),
]);

const appStore = enhancedCreateStore<AppState, AppAction>({
  count: 0,
  user: null,
  theme: 'light',
  notifications: [],
});

// dispatch 普通 action
appStore.dispatch(actions.increment(1));

// dispatch thunk(异步)
appStore.dispatch(async (dispatch, getState) => {
  dispatch(actions.increment(1));
  await new Promise((r) => setTimeout(r, 1000));
  dispatch(actions.setTheme('dark'));
});

示例 6:immer 不可变更新

// immer-usage.ts - 使用 immer 简化深层嵌套的不可变更新
import { produce, current, freeze, original } from 'immer';

interface DeepState {
  user: {
    profile: {
      name: string;
      preferences: {
        theme: 'light' | 'dark';
        language: 'zh' | 'en';
      };
    };
    activities: Array<{ type: string; timestamp: number }>;
  };
  settings: {
    notifications: boolean;
  };
}

const deepState: DeepState = {
  user: {
    profile: {
      name: 'Alice',
      preferences: { theme: 'light', language: 'zh' },
    },
    activities: [],
  },
  settings: { notifications: true },
};

// 传统写法:深层嵌套的展开运算符,可读性差
const traditionalUpdate: DeepState = {
  ...deepState,
  user: {
    ...deepState.user,
    profile: {
      ...deepState.user.profile,
      preferences: {
        ...deepState.user.profile.preferences,
        theme: 'dark',
      },
    },
  },
};

// immer 写法:可变 API,自动生成不可变结果
const immerUpdate: DeepState = produce(deepState, (draft) => {
  // 直接修改 draft,immer 内部通过 Proxy 拦截
  draft.user.profile.preferences.theme = 'dark';
  draft.user.activities.push({ type: 'login', timestamp: Date.now() });
});

// 原状态不变
console.log(deepState.user.profile.preferences.theme);  // 'light'
console.log(immerUpdate.user.profile.preferences.theme);  // 'dark'

// 结构共享:未修改的分支引用相同
console.log(deepState.settings === immerUpdate.settings);  // true
console.log(deepState.user.profile.name === immerUpdate.user.profile.name);  // true

// 使用 current 查看当前 draft 的快照(用于调试)
const withCurrent = produce(deepState, (draft) => {
  draft.user.profile.name = 'Bob';
  console.log(current(draft.user.profile));  // { name: 'Bob', ... }
  console.log(original(draft.user.profile));  // 原始对象(未修改)
});

// 使用 freeze 冻结状态,防止意外修改(生产环境推荐)
const frozenState = freeze(deepState, true);  // 深度冻结
// frozenState.user.profile.name = 'X';  // TypeError: Cannot assign to read only property

示例 7:Zustand 极简实践

// zustand-store.ts - Zustand 极简状态管理
// 1KB 体积,API 友好,TypeScript 原生支持
import { create } from 'zustand';
import { devtools, persist } from 'zustand/middleware';
import { immer } from 'zustand/middleware/immer';

/**
 * Store 类型定义
 * 包含状态、操作方法
 */
interface BearStore {
  // 状态
  bears: number;
  users: Array<{ id: string; name: string }>;
  // 操作
  increase: (by: number) => void;
  decrease: (by: number) => void;
  addUser: (user: { id: string; name: string }) => void;
  removeUser: (id: string) => void;
  reset: () => void;
}

/**
 * 创建 Store
 * 中间件顺序:immer(最内层)→ devtools → persist(最外层)
 */
const useBearStore = create<BearStore>()(
  devtools(
    persist(
      immer((set) => ({
        bears: 0,
        users: [],

        // immer 中间件允许直接修改 draft
        increase: (by) =>
          set((state) => {
            state.bears += by;
          }),

        decrease: (by) =>
          set((state) => {
            state.bears -= by;
          }),

        addUser: (user) =>
          set((state) => {
            state.users.push(user);
          }),

        removeUser: (id) =>
          set((state) => {
            state.users = state.users.filter((u) => u.id !== id);
          }),

        reset: () =>
          set((state) => {
            state.bears = 0;
            state.users = [];
          }),
      })),
      {
        name: 'bear-storage',  // localStorage 键名
        partialize: (state) => ({ bears: state.bears }),  // 仅持久化 bears
      },
    ),
    { name: 'BearStore' },  // DevTools 标识
  ),
);

// 在组件中使用
function BearCounter() {
  // 选择器:仅订阅 bears,避免不必要重渲染
  const bears = useBearStore((state) => state.bears);
  const increase = useBearStore((state) => state.increase);
  return (
    <div>
      <span>Bears: {bears}</span>
      <button onClick={() => increase(1)}>+1</button>
    </div>
  );
}

// 批量订阅(浅比较优化)
function UserList() {
  const { users, addUser, removeUser } = useBearStore(
    (state) => ({
      users: state.users,
      addUser: state.addUser,
      removeUser: state.removeUser,
    }),
    // 浅比较:避免每次创建新对象触发重渲染
    (prev, next) =>
      prev.users === next.users &&
      prev.addUser === next.addUser &&
      prev.removeUser === next.removeUser,
  );
  // ...
}

// 在组件外使用(非 React 上下文)
useBearStore.getState().increase(5);
useBearStore.subscribe((state, prevState) => {
  console.log('Bears changed:', prevState.bears, '->', state.bears);
});

示例 8:XState 有限状态机

// machine.ts - XState 有限状态机
// 将复杂业务流程建模为状态机,确保"非法状态不可表达"
import { createMachine, assign, interpret, ActorRefFrom } from 'xstate';
import { createActorContext } from '@xstate/react';

/**
 * 订单状态机
 * 状态:idle → selecting → checkout → paying → success / failed
 * 事件:START_CHECKOUT, SUBMIT_ORDER, PAYMENT_SUCCESS, PAYMENT_FAILED, CANCEL, RETRY
 */
type OrderContext = {
  items: ReadonlyArray<{ id: string; quantity: number }>;
  totalAmount: number;
  errorMessage?: string;
};

type OrderEvent =
  | { type: 'START_CHECKOUT' }
  | { type: 'SUBMIT_ORDER'; items: OrderContext['items'] }
  | { type: 'PAYMENT_SUCCESS' }
  | { type: 'PAYMENT_FAILED'; error: string }
  | { type: 'CANCEL' }
  | { type: 'RETRY' };

type OrderState =
  | { value: 'idle'; context: OrderContext }
  | { value: 'selecting'; context: OrderContext }
  | { value: 'checkout'; context: OrderContext }
  | { value: 'paying'; context: OrderContext }
  | { value: 'success'; context: OrderContext }
  | { value: 'failed'; context: OrderContext };

const orderMachine = createMachine<OrderContext, OrderEvent, OrderState>({
  id: 'order',
  initial: 'idle',
  context: {
    items: [],
    totalAmount: 0,
  },
  states: {
    idle: {
      on: {
        START_CHECKOUT: 'selecting',
      },
    },
    selecting: {
      on: {
        SUBMIT_ORDER: {
          target: 'checkout',
          actions: assign({
            items: (_, event) => event.items,
            totalAmount: (context) =>
              context.items.reduce((sum, item) => sum + item.quantity * 100, 0),
          }),
        },
        CANCEL: 'idle',
      },
    },
    checkout: {
      on: {
        PAYMENT_SUCCESS: 'success',
        PAYMENT_FAILED: {
          target: 'failed',
          actions: assign({
            errorMessage: (_, event) => event.error,
          }),
        },
        CANCEL: 'selecting',
      },
    },
    // 注意:paying 状态被省略,状态机直接从 checkout 跳转到 success/failed
    // 这避免了"已支付但未确认"的中间状态
    success: {
      type: 'final',  // 终态
    },
    failed: {
      on: {
        RETRY: 'checkout',
        CANCEL: 'idle',
      },
    },
  },
});

// React 集成
const OrderActorContext = createActorContext(orderMachine);

function OrderPage() {
  return (
    <OrderActorContext.Provider>
      <OrderFlow />
    </OrderActorContext.Provider>
  );
}

function OrderFlow() {
  const state = OrderActorContext.useSelector((s) => s.value);
  const context = OrderActorContext.useSelector((s) => s.context);
  const send = OrderActorContext.useActorRef().send;

  switch (state) {
    case 'idle':
      return <button onClick={() => send({ type: 'START_CHECKOUT' })}>开始下单</button>;
    case 'selecting':
      return (
        <div>
          <button onClick={() => send({ type: 'SUBMIT_ORDER', items: [{ id: '1', quantity: 2 }] })}>
            提交订单
          </button>
          <button onClick={() => send({ type: 'CANCEL' })}>取消</button>
        </div>
      );
    case 'checkout':
      return <div>处理支付中... 总额: ¥{context.totalAmount}</div>;
    case 'success':
      return <div>支付成功!</div>;
    case 'failed':
      return (
        <div>
          支付失败:{context.errorMessage}
          <button onClick={() => send({ type: 'RETRY' })}>重试</button>
          <button onClick={() => send({ type: 'CANCEL' })}>取消</button>
        </div>
      );
  }
}

示例 9:Jotai 原子状态

// atoms.ts - Jotai 原子状态管理
// 细粒度订阅,仅依赖变化的组件重渲染
import { atom, useAtom, useAtomValue, useSetAtom } from 'jotai';
import { atomWithStorage, selectAtom } from 'jotai/utils';

/**
 * 基础原子:独立的状态单元
 */
const countAtom = atom(0);
const userAtom = atom<{ id: string; name: string } | null>(null);

/**
 * 派生原子:从其他原子计算
 * 仅当依赖原子变化时才重新计算
 */
const greetingAtom = atom((get) => {
  const count = get(countAtom);
  const user = get(userAtom);
  return user ? `${user.name},计数 ${count}` : `访客,计数 ${count}`;
});

/**
 * 可写派生原子:双向转换
 */
const doubledCountAtom = atom(
  (get) => get(countAtom) * 2,
  (get, set, newDouble: number) => {
    set(countAtom, newDouble / 2);
  },
);

/**
 * 持久化原子:自动同步 localStorage
 */
const themeAtom = atomWithStorage<'light' | 'dark'>('theme', 'light');

/**
 * 选择派生:从复杂对象中选取字段
 * selectAtom 自动记忆化,避免重渲染
 */
const userNameAtom = selectAtom(userAtom, (user) => user?.name ?? '未登录');

// 在组件中使用
function CountDisplay() {
  const [count, setCount] = useAtom(countAtom);
  return (
    <div>
      <span>{count}</span>
      <button onClick={() => setCount((c) => c + 1)}>+</button>
    </div>
  );
}

function Greeting() {
  const greeting = useAtomValue(greetingAtom);  // 只读
  return <h1>{greeting}</h1>;
}

function ThemeToggle() {
  const [theme, setTheme] = useAtom(themeAtom);
  return (
    <button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
      切换主题(当前: {theme})
    </button>
  );
}

// 性能优势:1000 个组件订阅不同原子,更新 countAtom 仅触发订阅它的组件

对比分析

表 1:主流状态管理方案对比

方案体积学习曲线类型安全性能生态适用场景
Redux Toolkit~14KB陡峭优秀中等极丰富大型团队协作、需时间旅行
Zustand~1KB平缓优秀优秀适中中小型应用、追求极简
Jotai~3KB平缓优秀极佳适中细粒度订阅、复杂依赖图
Pinia~2KB平缓优秀优秀Vue 生态Vue 3 应用首选
MobX~16KB中等良好优秀适中OOP 风格、自动响应
Valtio~3KB平缓良好优秀适中可变 API 偏好
XState~30KB陡峭优秀中等适中复杂业务流程、FSM 建模
React Context0KB平缓手动内置简单主题、用户信息

选型建议

  • 小型应用(<10 组件):React Context + useState 足矣。
  • 中型应用(10-100 组件):Zustand 或 Jotai,按需选择。
  • 大型应用(100+ 组件):Redux Toolkit + React Query(服务端状态分离)。
  • 复杂流程(多步表单、支付、聊天):XState 建模状态机。
  • Vue 3 应用:Pinia 是事实标准。
  • 追求细粒度性能:Jotai 或 Solid Signals。

表 2:状态范式对比

范式数据流订阅粒度调试性测试性代表方案
全局 Store(Redux)单向拉取选择器极佳(时间旅行)极佳Redux、Zustand
原子状态(Atomic)单向拉取原子级良好良好Jotai、Recoil
响应式对象(Reactive)双向推送属性级中等中等MobX、Vue Reactivity
Signal推拉混合信号级中等良好Solid、Angular、Svelte 5
FSM事件驱动状态级极佳极佳XState、Robot
Proxy 可变推送属性级中等中等Valtio、Immer 配合

权衡分析

  • 调试性:Redux 的纯函数 reducer 使时间旅行成为可能,FSM 的状态图可视化极佳。
  • 性能:Signal 与原子状态的细粒度订阅在大规模应用中优势明显。
  • 可测试性:纯函数 reducer 与 FSM 的状态转移函数最易单元测试。
  • 学习曲线:Zustand 与 Context 最易上手,XState 与 Redux 需要学习成本。

表 3:不可变更新方案对比

方案API 风格性能包体积学习曲线适用场景
展开运算符不可变中等0KB平缓浅层状态
immer可变 API优秀~5KB平缓深层嵌套
immutable.js不可变极佳~60KB陡峭超大规模
Ramda函数式良好~40KB陡峭函数式偏好
Structura不可变极佳~3KB平缓性能敏感
immer + Zustand可变 API优秀~6KB平缓推荐

推荐

  • 默认使用 immer,可变 API 降低心智负担,性能足够。
  • 超大规模(万级节点)考虑 immutable.jsStructura
  • 避免手写深拷贝,性能与正确性都难以保证。

常见陷阱与反模式

陷阱 1:直接修改状态

问题:直接修改 state 对象,React 不会感知变化,UI 不更新。

// 反模式:直接修改
store.setState((state) => {
  state.count += 1;  // 直接修改原对象
  return state;       // 返回同一引用
});
// 结果:React 认为状态未变,不重渲染

修复:始终返回新对象。

// 正确:不可变更新
store.setState((state) => ({ ...state, count: state.count + 1 }));
// 或使用 immer
store.setState(produce(state, (draft) => { draft.count += 1; }));

陷阱 2:选择器返回新对象

问题:选择器每次调用都返回新对象,即使内容相同,引用也不同,导致不必要的重渲染。

// 反模式:每次返回新数组
const selectUnread = (state: AppState) =>
  state.notifications.filter((n) => !n.read);  // 每次新数组

// 结果:useStoreSelector 每次都认为状态变了,触发重渲染

修复:使用记忆化或浅比较。

// 正确:使用 reselect 记忆化
const selectUnread = createSelector(
  [(state) => state.notifications],
  (notifications) => notifications.filter((n) => !n.read),
);

// 或在组件中传入浅比较函数
const unread = useStoreSelector(store, selectUnread, shallowEqual);

陷阱 3:在 Reducer 中执行副作用

问题:在 reducer 中发起网络请求、修改全局变量、调用 localStorage,破坏纯函数约束。

// 反模式:reducer 中有副作用
function badReducer(state: AppState, action: AppAction): AppState {
  switch (action.type) {
    case 'FETCH_USER':
      fetch('/api/user').then(res => res.json()).then(user => {
        store.dispatch({ type: 'SET_USER', payload: user });
      });
      return state;
  }
}

后果

  • 时间旅行失效(回放会重复发起请求)。
  • 测试困难(reducer 依赖网络环境)。
  • 并发问题(多个相同 action 同时执行)。

修复:副作用放在中间件(thunk、saga)或 useEffect 中。

陷阱 4:过度使用全局状态

问题:将所有状态都放入全局 Store,包括组件局部状态(如表单输入、临时 UI 状态)。

后果

  • Store 膨胀,难以维护。
  • 不必要的全局订阅,性能下降。
  • 组件复用性降低(依赖全局状态)。

修复:遵循”状态归属”原则。

  • 局部状态(仅当前组件用):useStateuseReducer
  • 共享状态(多个组件用):全局 Store 或 Context。
  • URL 状态(需分享/刷新):URL 参数 + nuqs
  • 服务端状态(来自 API):React Query、SWR。
  • 表单状态react-hook-formFormik

陷阱 5:状态嵌套过深

问题:状态树嵌套 5+ 层,更新时需要多层展开,可读性差且易错。

// 反模式:过深嵌套
interface BadState {
  ui: {
    pages: {
      home: {
        sidebar: {
          collapsed: boolean;
          width: number;
        };
      };
    };
  };
}
// 更新 collapsed 需要 4 层展开

修复:扁平化或使用 normalize。

// 正确:扁平化
interface GoodState {
  ui: {
    sidebar: { collapsed: boolean; width: number };
    currentPage: 'home' | 'about';
  };
}

// 或使用切片组合
const sidebarSlice = createSlice({
  name: 'sidebar',
  initialState: { collapsed: false, width: 200 },
  reducers: {
    toggle: (state) => { state.collapsed = !state.collapsed; },
  },
});

陷阱 6:循环订阅

问题:在订阅回调中 dispatch action,可能导致无限循环。

// 反模式:订阅中 dispatch
store.subscribe(() => {
  const state = store.getState();
  if (state.count > 10) {
    store.dispatch(actions.decrement(1));  // 又触发订阅,无限循环
  }
});

修复:使用条件判断或防抖。

// 正确:条件判断
store.subscribe(() => {
  const state = store.getState();
  if (state.count > 10 && !state.corrected) {
    store.dispatch(actions.setCorrected(true));
    store.dispatch(actions.decrement(state.count - 10));
  }
});

陷阱 7:忽略 SSR 水合

问题:服务端渲染时状态与客户端不一致,导致水合错误。

// 反模式:客户端与服务端状态不同步
const store = createStore({
  timestamp: Date.now(),  // 服务端与客户端时间不同
});

修复:使用 useSyncExternalStore 的第三个参数(getServerSnapshot)或在 useEffect 中初始化。

// 正确:SSR 友好
function useStore<S>(store: Store<S>): S {
  return useSyncExternalStore(
    store.subscribe,
    store.getState,
    store.getState,  // getServerSnapshot:返回服务端快照
  );
}

// 或在客户端水合后再设置时间敏感状态
function Timestamp() {
  const [time, setTime] = useState<number | null>(null);
  useEffect(() => {
    setTime(Date.now());
  }, []);
  return <div>{time ?? '加载中...'}</div>;
}

陷阱 8:DevTools 性能开销

问题:生产环境开启 DevTools 中间件,每次 dispatch 序列化整个状态,性能损失严重。

修复:仅在开发环境启用。

// 正确:条件启用
const store = createStore(
  reducer,
  initialState,
  compose(
    process.env.NODE_ENV === 'development' ? applyMiddleware(logger, devTools) : applyMiddleware(),
  ),
);

// 或使用 Redux DevTools Extension 的生产模式(仅记录 action 类型)
const composeEnhancers =
  typeof window !== 'undefined' && (window as any).__REDUX_DEVTOOLS_EXTENSION_COMPOSE__
    ? (window as any).__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({
        actionSanitizer: (action: AppAction) => ({ type: action.type }),  // 不记录 payload
      })
    : compose;

工程实践

实践 1:状态切片设计

将大型状态拆分为领域切片,每片独立管理。

// slices/userSlice.ts
import { createSlice, PayloadAction } from '@reduxjs/toolkit';

interface UserState {
  currentUser: { id: string; name: string; email: string } | null;
  isAuthenticated: boolean;
  loading: boolean;
  error: string | null;
}

const userSlice = createSlice({
  name: 'user',
  initialState: {
    currentUser: null,
    isAuthenticated: false,
    loading: false,
    error: null,
  } as UserState,
  reducers: {
    fetchUserStart: (state) => {
      state.loading = true;
      state.error = null;
    },
    fetchUserSuccess: (state, action: PayloadAction<UserState['currentUser']>) => {
      state.currentUser = action.payload;
      state.isAuthenticated = true;
      state.loading = false;
    },
    fetchUserFailure: (state, action: PayloadAction<string>) => {
      state.error = action.payload;
      state.loading = false;
    },
    logout: (state) => {
      state.currentUser = null;
      state.isAuthenticated = false;
    },
  },
});

export const { fetchUserStart, fetchUserSuccess, fetchUserFailure, logout } = userSlice.actions;
export default userSlice.reducer;

// store.ts - 组合切片
import { configureStore } from '@reduxjs/toolkit';
import userReducer from './slices/userSlice';
import cartReducer from './slices/cartSlice';
import uiReducer from './slices/uiSlice';

export const store = configureStore({
  reducer: {
    user: userReducer,
    cart: cartReducer,
    ui: uiReducer,
  },
  middleware: (getDefault) => getDefault().concat(logger, persistence),
  devTools: process.env.NODE_ENV !== 'production',
});

// 类型导出: RootState 与 AppDispatch 自动推导
export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;

实践 2:Typed Hooks 封装

为 Redux 封装类型安全的 Hooks,避免重复写类型。

// hooks.ts - 类型安全的 Hooks 封装
import { useDispatch, useSelector, TypedUseSelectorHook } from 'react-redux';
import type { RootState, AppDispatch } from './store';

// 替代 useDispatch,自动推导 Action 类型
export const useAppDispatch = () => useDispatch<AppDispatch>();
// 替代 useSelector,自动推导 State 类型
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;

// 使用:无需手动标注类型
function UserProfile() {
  const user = useAppSelector((state) => state.user.currentUser);  // 类型自动推导
  const dispatch = useAppDispatch();
  // dispatch 自动校验 Action 类型
  return <button onClick={() => dispatch(logout())}>退出</button>;
}

实践 3:异步操作标准化

使用 Redux Toolkit 的 createAsyncThunk 标准化异步操作。

// asyncActions.ts - 异步 Action 标准化
import { createAsyncThunk } from '@reduxjs/toolkit';

/**
 * 登录异步 Action
 * 自动生成 pending/fulfilled/rejected 三种 Action
 */
export const login = createAsyncThunk(
  'user/login',
  async (credentials: { email: string; password: string }, { rejectWithValue }) => {
    try {
      const response = await fetch('/api/auth/login', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(credentials),
      });
      if (!response.ok) {
        const error = await response.json();
        return rejectWithValue(error.message);
      }
      return (await response.json()) as { id: string; name: string; email: string; token: string };
    } catch (err) {
      return rejectWithValue('网络错误');
    }
  },
);

// 在 slice 中处理
const userSlice = createSlice({
  name: 'user',
  initialState,
  reducers: { /* 同步 reducers */ },
  extraReducers: (builder) => {
    builder
      .addCase(login.pending, (state) => {
        state.loading = true;
        state.error = null;
      })
      .addCase(login.fulfilled, (state, action) => {
        state.currentUser = action.payload;
        state.isAuthenticated = true;
        state.loading = false;
      })
      .addCase(login.rejected, (state, action) => {
        state.error = action.payload as string;
        state.loading = false;
      });
  },
});

实践 4:状态持久化与水合

// persistence.ts - 状态持久化与水合
import { persistReducer, persistStore } from 'redux-persist';
import storage from 'redux-persist/lib/storage';
import { rootReducer } from './reducers';

/**
 * 持久化配置
 * 仅持久化白名单字段,避免敏感数据泄漏
 */
const persistConfig = {
  key: 'root',
  storage,
  whitelist: ['user', 'theme'],  // 仅持久化这些切片
  blacklist: ['temporary', 'loading'],  // 不持久化这些
  transforms: [
    // 自定义转换:加密敏感数据
    {
      in: (state: any, key: string) => {
        if (key === 'user' && state?.token) {
          return { ...state, token: encrypt(state.token) };
        }
        return state;
      },
      out: (state: any, key: string) => {
        if (key === 'user' && state?.token) {
          return { ...state, token: decrypt(state.token) };
        }
        return state;
      },
    },
  ],
};

const persistedReducer = persistReducer(persistConfig, rootReducer);

// 水合检测
import { PersistGate } from 'redux-persist/integration/react';

function App() {
  return (
    <Provider store={store}>
      <PersistGate loading={<div>加载中...</div>} persistor={persistor}>
        <Router />
      </PersistGate>
    </Provider>
  );
}

实践 5:测试金字塔

// __tests__/reducer.test.ts - Reducer 单元测试
import { appReducer, actions } from '../store';
import type { AppState } from '../store';

describe('appReducer', () => {
  const initialState: AppState = {
    count: 0,
    user: null,
    theme: 'light',
    notifications: [],
  };

  it('INCREMENT 应增加 count', () => {
    const newState = appReducer(initialState, actions.increment(5));
    expect(newState.count).toBe(5);
    expect(initialState.count).toBe(0);  // 原状态不变
  });

  it('SET_USER 应设置用户', () => {
    const newState = appReducer(
      initialState,
      actions.setUser('1', 'Alice'),
    );
    expect(newState.user).toEqual({ id: '1', name: 'Alice' });
  });

  it('未知 action 应返回原状态', () => {
    const action = { type: 'UNKNOWN' } as any;
    const newState = appReducer(initialState, action);
    expect(newState).toBe(initialState);
  });

  it('ADD_NOTIFICATION 应添加到数组末尾', () => {
    const state = { ...initialState, notifications: [{ id: '1', message: 'A', read: false }] };
    const newState = appReducer(state, actions.addNotification('B'));
    expect(newState.notifications).toHaveLength(2);
    expect(newState.notifications[1].message).toBe('B');
  });
});

// __tests__/selector.test.ts - Selector 测试
import { selectUnreadCount, selectUserGreeting } from '../selectors';

describe('selectors', () => {
  it('selectUnreadCount 应正确计算未读数', () => {
    const state = {
      notifications: [
        { id: '1', message: 'A', read: false },
        { id: '2', message: 'B', read: true },
        { id: '3', message: 'C', read: false },
      ],
    } as any;
    expect(selectUnreadCount(state)).toBe(2);
  });

  it('selectUserGreeting 应正确生成问候语', () => {
    const stateWithUser = {
      count: 5,
      user: { id: '1', name: 'Alice' },
    } as any;
    expect(selectUserGreeting(stateWithUser)).toBe('Alice,计数 5');

    const stateWithoutUser = { count: 3, user: null } as any;
    expect(selectUserGreeting(stateWithoutUser)).toBe('访客,计数 3');
  });
});

// __tests__/integration.test.tsx - 集成测试
import { render, screen, fireEvent } from '@testing-library/react';
import { Provider } from 'react-redux';
import { store } from '../store';
import { Counter } from '../Counter';

describe('Counter 集成', () => {
  it('点击按钮应增加计数', () => {
    render(
      <Provider store={store}>
        <Counter />
      </Provider>,
    );
    expect(screen.getByText('Count: 0')).toBeInTheDocument();
    fireEvent.click(screen.getByText('+1'));
    expect(screen.getByText('Count: 1')).toBeInTheDocument();
  });
});

实践 6:DevTools 集成

// devtools.ts - Redux DevTools 配置
import { configureStore, Action } from '@reduxjs/toolkit';

const isDev = process.env.NODE_ENV === 'development';

export const store = configureStore({
  reducer: rootReducer,
  middleware: (getDefault) =>
    getDefault().concat(
      // 开发环境添加日志
      isDev ? logger : (store) => (next) => (action: Action) => next(action),
    ),
  devTools: isDev
    ? {
        name: 'FANDEX App',
        maxAge: 50,  // 最多保留 50 个 action
        actionSanitizer: (action: any) => ({
          type: action.type,
          // 脱敏:不记录密码等敏感字段
          payload: action.payload?.password ? { ...action.payload, password: '***' } : action.payload,
        }),
        stateSanitizer: (state: any) => ({
          ...state,
          // 脱敏:不记录完整 token
          user: state.user ? { ...state.user, token: state.user.token ? '***' : null } : null,
        }),
        trace: true,  // 记录调用栈
        traceLimit: 25,
      }
    : false,
});

案例研究

案例 1:电商购物车(Redux Toolkit)

场景:电商应用的购物车,需支持添加/删除商品、调整数量、计算总价、持久化。

架构:使用 Redux Toolkit + immer + persist。

// cartSlice.ts
import { createSlice, PayloadAction, createEntityAdapter } from '@reduxjs/toolkit';

interface CartItem {
  id: string;
  name: string;
  price: number;
  quantity: number;
}

// 使用 EntityAdapter 规范化存储(类似数据库表)
const cartAdapter = createEntityAdapter<CartItem>({
  selectId: (item) => item.id,
  sortComparer: (a, b) => a.name.localeCompare(b.name),
});

const cartSlice = createSlice({
  name: 'cart',
  initialState: cartAdapter.getInitialState({ totalAmount: 0, totalQuantity: 0 }),
  reducers: {
    addItem: {
      reducer: (state, action: PayloadAction<CartItem>) => {
        const existing = state.entities[action.payload.id];
        if (existing) {
          existing.quantity += action.payload.quantity;
        } else {
          cartAdapter.addOne(state, action);
        }
        state.totalQuantity += action.payload.quantity;
        state.totalAmount += action.payload.price * action.payload.quantity;
      },
      prepare: (item: Omit<CartItem, 'quantity'> & { quantity?: number }) => ({
        payload: { ...item, quantity: item.quantity ?? 1 } as CartItem,
      }),
    },
    removeItem: (state, action: PayloadAction<string>) => {
      const item = state.entities[action.payload];
      if (item) {
        state.totalQuantity -= item.quantity;
        state.totalAmount -= item.price * item.quantity;
        cartAdapter.removeOne(state, action);
      }
    },
    updateQuantity: (state, action: PayloadAction<{ id: string; quantity: number }>) => {
      const { id, quantity } = action.payload;
      const item = state.entities[id];
      if (item && quantity > 0) {
        state.totalQuantity += quantity - item.quantity;
        state.totalAmount += (quantity - item.quantity) * item.price;
        item.quantity = quantity;
      }
    },
    clearCart: (state) => {
      cartAdapter.removeAll(state);
      state.totalAmount = 0;
      state.totalQuantity = 0;
    },
  },
});

// 选择器
const cartSelectors = cartAdapter.getSelectors((state: RootState) => state.cart);

export const selectCartItems = cartSelectors.selectAll;
export const selectCartTotal = (state: RootState) => state.cart.totalAmount;
export const selectCartCount = (state: RootState) => state.cart.totalQuantity;
export const selectItemById = cartSelectors.selectById;

收益:EntityAdapter 规范化存储避免重复项,immer 自动处理不可变更新,persist 持久化购物车跨会话。

案例 2:实时协作编辑(Yjs + Zustand)

场景:多人实时协作的文档编辑器,需同步光标位置、文本内容、用户在线状态。

架构:Yjs(CRDT 库)+ Zustand(UI 状态)。

// collaborativeStore.ts
import { create } from 'zustand';
import * as Y from 'yjs';
import { WebsocketProvider } from 'y-websocket';

interface CollabState {
  doc: Y.Doc;
  provider: WebsocketProvider;
  onlineUsers: Array<{ id: string; name: string; color: string; cursor: { line: number; column: number } | null }>;
  isConnected: boolean;
  setOnlineUsers: (users: CollabState['onlineUsers']) => void;
  setConnected: (connected: boolean) => void;
}

const useCollabStore = create<CollabState>((set) => {
  const doc = new Y.Doc();
  const provider = new WebsocketProvider('wss://collab.example.com', 'doc-123', doc);

  provider.awareness.on('change', () => {
    const users = Array.from(provider.awareness.getStates().values()).map((state: any) => ({
      id: state.user.id,
      name: state.user.name,
      color: state.user.color,
      cursor: state.cursor ?? null,
    }));
    set({ onlineUsers: users });
  });

  provider.on('status', (event: { status: string }) => {
    set({ isConnected: event.status === 'connected' });
  });

  return {
    doc,
    provider,
    onlineUsers: [],
    isConnected: false,
    setOnlineUsers: (users) => set({ onlineUsers: users }),
    setConnected: (connected) => set({ isConnected: connected }),
  };
});

// Yjs 负责 CRDT 同步(文本内容),Zustand 负责 UI 状态(光标、在线列表)
// 两者分离,各司其职

收益:CRDT 保证最终一致性,Zustand 管理 UI 派生状态,职责清晰。

案例 3:游戏状态管理(XState)

场景:回合制游戏的状态机,包含准备、玩家回合、敌人回合、胜利、失败等状态。

// gameMachine.ts
import { createMachine, assign, ActorRefFrom } from 'xstate';

interface GameContext {
  playerHp: number;
  enemyHp: number;
  turn: number;
  log: string[];
}

type GameEvent =
  | { type: 'START' }
  | { type: 'PLAYER_ATTACK'; damage: number }
  | { type: 'ENEMY_ATTACK'; damage: number }
  | { type: 'PLAYER_HEAL'; amount: number }
  | { type: 'RESTART' };

const gameMachine = createMachine({
  id: 'game',
  initial: 'menu',
  context: {
    playerHp: 100,
    enemyHp: 100,
    turn: 1,
    log: [],
  } as GameContext,
  states: {
    menu: {
      on: { START: 'playerTurn' },
    },
    playerTurn: {
      on: {
        PLAYER_ATTACK: {
          target: 'checkResult',
          actions: assign({
            enemyHp: (ctx, e: any) => Math.max(0, ctx.enemyHp - e.damage),
            log: (ctx, e) => [...ctx.log, `玩家攻击,造成 ${e.damage} 伤害`],
          }),
        },
        PLAYER_HEAL: {
          target: 'enemyTurn',
          actions: assign({
            playerHp: (ctx, e: any) => Math.min(100, ctx.playerHp + e.amount),
            log: (ctx, e) => [...ctx.log, `玩家治疗,恢复 ${e.amount} 生命`],
          }),
        },
      },
    },
    enemyTurn: {
      after: {
        1000: {
          target: 'checkResult',
          actions: assign({
            playerHp: (ctx) => Math.max(0, ctx.playerHp - 15),
            turn: (ctx) => ctx.turn + 1,
            log: (ctx) => [...ctx.log, '敌人攻击,造成 15 伤害'],
          }),
        },
      },
    },
    checkResult: {
      always: [
        {
          guard: (ctx) => ctx.enemyHp <= 0,
          target: 'victory',
        },
        {
          guard: (ctx) => ctx.playerHp <= 0,
          target: 'defeat',
        },
        {
          target: 'playerTurn',
        },
      ],
    },
    victory: {
      on: { RESTART: 'menu' },
    },
    defeat: {
      on: { RESTART: 'menu' },
    },
  },
});

收益:状态机保证非法状态(如玩家已死亡但仍能攻击)不可表达,always + guard 实现自动状态转移。

案例 4:表单状态(React Hook Form + Zod)

场景:复杂的多步表单,需验证、跨步骤数据保留、提交。

// multiStepForm.ts
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';

// Schema 驱动:类型与校验同源
const formSchema = z.object({
  // 步骤 1:基本信息
  name: z.string().min(2, '姓名至少 2 字符'),
  email: z.string().email('邮箱格式无效'),
  // 步骤 2:地址
  address: z.object({
    street: z.string().min(5),
    city: z.string().min(2),
    zipCode: z.string().regex(/^\d{6}$/, '邮编为 6 位数字'),
  }),
  // 步骤 3:偏好
  preferences: z.object({
    newsletter: z.boolean(),
    theme: z.enum(['light', 'dark']),
  }),
});

type FormData = z.infer<typeof formSchema>;

function MultiStepForm() {
  const [step, setStep] = useState(0);
  const form = useForm<FormData>({
    resolver: zodResolver(formSchema),
    mode: 'onChange',  // 实时校验
    defaultValues: {
      name: '',
      email: '',
      address: { street: '', city: '', zipCode: '' },
      preferences: { newsletter: true, theme: 'light' },
    },
  });

  const onNext = async () => {
    // 仅校验当前步骤的字段
    const fields = step === 0 ? ['name', 'email'] :
                   step === 1 ? ['address.street', 'address.city', 'address.zipCode'] :
                   ['preferences.newsletter', 'preferences.theme'];
    const valid = await form.trigger(fields as any);
    if (valid) setStep((s) => s + 1);
  };

  const onSubmit = (data: FormData) => {
    console.log('提交:', data);
  };

  return (
    <form onSubmit={form.handleSubmit(onSubmit)}>
      {step === 0 && (
        <>
          <input {...form.register('name')} placeholder="姓名" />
          {form.formState.errors.name && <span>{form.formState.errors.name.message}</span>}
          <input {...form.register('email')} placeholder="邮箱" />
        </>
      )}
      {step === 1 && (
        <>
          <input {...form.register('address.street')} placeholder="街道" />
          <input {...form.register('address.city')} placeholder="城市" />
          <input {...form.register('address.zipCode')} placeholder="邮编" />
        </>
      )}
      {step === 2 && (
        <>
          <label>
            <input type="checkbox" {...form.register('preferences.newsletter')} />
            订阅通讯
          </label>
          <select {...form.register('preferences.theme')}>
            <option value="light">浅色</option>
            <option value="dark">深色</option>
          </select>
        </>
      )}
      {step < 2 ? (
        <button type="button" onClick={onNext}>下一步</button>
      ) : (
        <button type="submit">提交</button>
      )}
    </form>
  );
}

收益:Zod Schema 同时驱动类型与校验,React Hook Form 提供高性能表单状态管理,避免 Controlled Component 的重渲染问题。

案例 5:服务端状态分离(React Query)

场景:新闻列表应用,需缓存、分页、失效重取、乐观更新。

// useArticles.ts - 服务端状态管理
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';

interface Article {
  id: string;
  title: string;
  content: string;
  authorId: string;
  publishedAt: string;
}

/**
 * 获取文章列表
 * 自动缓存、去重、后台刷新
 */
function useArticles(page: number) {
  return useQuery({
    queryKey: ['articles', page],
    queryFn: async () => {
      const res = await fetch(`/api/articles?page=${page}`);
      if (!res.ok) throw new Error('获取失败');
      return res.json() as Promise<{ items: Article[]; total: number }>;
    },
    staleTime: 5 * 60 * 1000,  // 5 分钟内不重新获取
    cacheTime: 30 * 60 * 1000,  // 缓存保留 30 分钟
    placeholderData: (prev) => prev,  // 翻页时保留旧数据
  });
}

/**
 * 创建文章(乐观更新)
 */
function useCreateArticle() {
  const queryClient = useQueryClient();

  return useMutation({
    mutationFn: async (newArticle: { title: string; content: string }) => {
      const res = await fetch('/api/articles', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(newArticle),
      });
      return res.json() as Promise<Article>;
    },
    // 乐观更新:立即更新 UI,失败后回滚
    onMutate: async (newArticle) => {
      await queryClient.cancelQueries({ queryKey: ['articles'] });
      const previousData = queryClient.getQueryData(['articles', 1]);

      queryClient.setQueryData(['articles', 1], (old: any) => ({
        ...old,
        items: [
          { id: 'temp-' + Date.now(), ...newArticle, authorId: 'me', publishedAt: new Date().toISOString() },
          ...old.items,
        ],
      }));

      return { previousData };
    },
    onError: (err, newArticle, context) => {
      // 失败回滚
      queryClient.setQueryData(['articles', 1], context?.previousData);
    },
    onSettled: () => {
      // 无论成功失败,重新获取最新数据
      queryClient.invalidateQueries({ queryKey: ['articles'] });
    },
  });
}

收益:服务端状态与客户端状态分离,React Query 处理缓存、重试、去重、乐观更新,全局 Store 只管 UI 状态。

案例 6:微前端状态隔离

场景:微前端架构下,多个子应用各自管理状态,通过事件总线通信。

// microFrontendState.ts - 微前端状态隔离
import { create } from 'zustand';

// 每个子应用有独立的 Store,不共享状态
const useApp1Store = create((set) => ({ /* ... */ }));
const useApp2Store = create((set) => ({ /* ... */ }));

// 跨应用通信通过事件总线,而非共享 Store
type GlobalEvent =
  | { type: 'USER_LOGOUT' }
  | { type: 'NAVIGATE'; path: string }
  | { type: 'THEME_CHANGE'; theme: 'light' | 'dark' };

class EventBus {
  private listeners = new Map<string, Set<(event: GlobalEvent) => void>>();

  on(type: string, listener: (event: GlobalEvent) => void) {
    if (!this.listeners.has(type)) this.listeners.set(type, new Set());
    this.listeners.get(type)!.add(listener);
    return () => this.listeners.get(type)?.delete(listener);
  }

  emit(event: GlobalEvent) {
    this.listeners.get(event.type)?.forEach((l) => l(event));
  }
}

export const eventBus = new EventBus();

// 子应用 A 监听全局事件
function MicroAppA() {
  useEffect(() => {
    return eventBus.on('USER_LOGOUT', () => {
      useApp1Store.getState().clearUser();
    });
  }, []);
  // ...
}

// 子应用 B 触发全局事件
function MicroAppB() {
  const handleLogout = () => {
    eventBus.emit({ type: 'USER_LOGOUT' });
  };
  // ...
}

收益:微前端状态隔离避免冲突,事件总线实现松耦合通信。

习题

基础题

1. 解释 Redux 的三大原则,并说明为何 reducer 必须是纯函数。

参考答案要点

  • 三大原则:单一数据源、状态只读、纯函数变更。
  • 纯函数的必要性:可预测性(相同输入相同输出)、可序列化(状态历史可回放)、时间旅行可行性(无副作用可任意回放)。

2. 下列代码有何问题?请修复。

const store = createStore({ users: [] });
store.setState((state) => {
  state.users.push({ id: 1, name: 'Alice' });
  return state;
});

参考答案要点

  • 问题:直接修改原 state,引用未变,React 不感知变化。
  • 修复:store.setState((state) => ({ users: [...state.users, { id: 1, name: 'Alice' }] }))

应用题

3. 实现一个类型安全的 createSlice 函数,自动生成 Action 创建器与 Reducer。

参考答案要点

function createSlice<S, R extends Record<string, (state: S, action: any) => S>>(config: {
  name: string;
  initialState: S;
  reducers: R;
}) {
  const actions = Object.fromEntries(
    Object.entries(config.reducers).map(([key, reducer]) => [
      key,
      (payload?: any) => ({ type: `${config.name}/${key}`, payload }),
    ]),
  ) as { [K in keyof R]: (payload: any) => { type: string; payload: any } };

  const reducer = (state: S = config.initialState, action: { type: string; payload?: any }) => {
    for (const [key, fn] of Object.entries(config.reducers)) {
      if (action.type === `${config.name}/${key}`) return fn(state, action);
    }
    return state;
  };

  return { name: config.name, actions, reducer };
}

4. 为以下状态设计 Selector,计算”每个用户的未读通知数”。

interface State {
  users: Array<{ id: string; name: string }>;
  notifications: Array<{ id: string; userId: string; read: boolean }>;
}

参考答案要点

const selectUserUnreadCounts = createSelector(
  [(state: State) => state.users, (state: State) => state.notifications],
  (users, notifications) =>
    users.map((user) => ({
      ...user,
      unreadCount: notifications.filter((n) => n.userId === user.id && !n.read).length,
    })),
);

分析题

5. 对比 Zustand 与 Redux Toolkit 在以下场景的优劣:100 个组件的中型应用,需时间旅行调试。

参考答案要点

  • Zustand:API 简洁,体积小,类型友好;但原生不支持时间旅行,需手动实现。
  • Redux Toolkit:原生支持时间旅行,生态丰富;但样板代码多,学习成本高。
  • 推荐:若时间旅行非刚需,Zustand 更轻量;若团队熟悉 Redux 或需复杂 DevTools,Redux Toolkit。

6. 下列 Selector 有何性能问题?如何优化?

const selectFiltered = (state: State) =>
  state.items.filter((item) => item.active).map((item) => item.name);

参考答案要点

  • 问题:每次调用都返回新数组,即使输入未变也触发重渲染。
  • 优化:使用 createSelector 记忆化,或传入浅比较函数。

评估题

7. 评估在 React 中使用 Context 替代 Redux 的可行性,列出至少 3 个限制。

参考答案要点

  • 限制 1:Context 值变化会触发所有消费者重渲染,无法细粒度订阅。
  • 限制 2:无中间件机制,难以扩展日志、持久化、异步。
  • 限制 3:无 DevTools 支持,调试困难。
  • 限制 4:无时间旅行,状态历史不可回放。
  • 适用场景:低频更新、少量消费者(如主题、用户信息)。

8. 评估 XState 在表单状态管理中的适用性,何时使用、何时避免?

参考答案要点

  • 适用:多步表单、有明确状态流转(如向导式表单)、需防止非法状态组合。
  • 避免:简单表单(少量字段、无复杂流转)、性能敏感场景(XState 体积较大)。
  • 替代方案:React Hook Form + Zod 适用于大多数表单。

创造题

9. 设计一个支持”撤销/重做”的状态管理方案,需考虑内存限制与性能。

参考答案要点

  • 核心思路:维护 past、present、future 三个状态栈。
  • 内存限制:限制历史栈长度(如 50),超出时丢弃最旧记录。
  • 性能优化:仅对”可撤销”的 action 入栈,非可撤销 action(如接收消息)不入栈。
  • 批量操作:连续的小步更新可合并为一次入栈(如拖拽过程中只入栈起止状态)。
  • 持久化:可选将历史栈持久化,支持跨会话撤销。
interface HistoryState<T> {
  past: T[];
  present: T;
  future: T[];
}

function undoable<S>(reducer: (state: S, action: any) => S): (state: HistoryState<S>, action: any) => HistoryState<S> {
  return (state, action) => {
    const { past, present, future } = state;
    switch (action.type) {
      case 'UNDO':
        if (past.length === 0) return state;
        return { past: past.slice(0, -1), present: past[past.length - 1], future: [present, ...future] };
      case 'REDO':
        if (future.length === 0) return state;
        return { past: [...past, present], present: future[0], future: future.slice(1) };
      default:
        const newPresent = reducer(present, action);
        if (newPresent === present) return state;
        const newPast = [...past, present].slice(-50);  // 限制 50 步
        return { past: newPast, present: newPresent, future: [] };
    }
  };
}

10. 为大型 SaaS 应用设计状态架构,涵盖用户、租户、权限、通知、实时数据、离线支持。

参考答案要点

  • 分层架构
    • 客户端 UI 状态:Zustand 或 Redux Toolkit(主题、侧边栏、当前路由)。
    • 服务端状态:React Query(用户信息、租户配置、通知列表)。
    • 实时数据:WebSocket + Zustand(在线状态、实时消息)。
    • 离线状态:IndexedDB + 同步队列(离线操作、冲突解决)。
  • 多租户隔离:每个租户独立的 Store 命名空间,切换租户时清理旧状态。
  • 权限管理:基于角色与资源的权限矩阵,缓存在 React Query 中,通过 selector 派生可操作菜单。
  • 通知系统:服务端推送 + 本地队列,支持优先级、分组、已读状态。
  • 离线支持:操作记录到 IndexedDB,恢复网络后批量同步,使用 CRDT 解决冲突。
  • 测试策略:reducer 单元测试、selector 单元测试、组件集成测试、E2E 测试(Playwright)。
  • 扩展点:预留插件机制(如自定义中间件)、状态切片动态加载(微前端场景)。

参考文献

  1. Abramov, D., & Clark, A. (2015). Redux: Predictable state container for JavaScript apps. GitHub Repository. https://github.com/reduxjs/redux. DOI: 10.5281/zenodo.4764471

  2. Wiggins, A. (2011). The Twelve-Factor App: Methodology for building modern web applications. Heroku. https://12factor.net/

  3. Harel, D. (1987). Statecharts: A visual formalism for complex systems. Science of Computer Programming, 8(3), 231-274. DOI: 10.1016/0167-6423(87)90035-9

  4. Odersky, M., Spoon, L., & Venners, B. (2019). Programming in Scala (5th ed.). Artima Press. ISBN: 978-0-9815316-4-2. (关于协变与逆变的类型理论)

  5. Edwards, J. (2014). Flux: Application architecture for building user interfaces. Facebook Engineering Blog. https://facebook.github.io/flux/

  6. Bini, A. (2019). Reactive Programming with RxJS 5. Packt Publishing. ISBN: 978-1-78646-821-0.

  7. Sicilia, A., Hu, T., & Sambasivan, M. (2018). When time matters: Analyzing user behaviors in web applications. Proceedings of the 2018 CHI Conference on Human Factors in Computing Systems, 1-12. DOI: 10.1145/3173574.3174195

  8. Fok, A., & McMillan, C. (2020). Comparative analysis of state management solutions in single-page applications. IEEE Software, 37(5), 42-49. DOI: 10.1109/MS.2020.2994061

  9. Carlo, D. (2021). Immer: Create the next immutable state by mutating the current one. GitHub. https://github.com/immerjs/immer

  10. Davis, M. (2018). XState: State machines and statecharts for the modern web. GitHub. https://github.com/statelyai/xstate

  11. Nygard, M. T. (2017). Release It!: Design and Deploy Production-Ready Software (2nd ed.). Pragmatic Bookshelf. ISBN: 978-1-68050-239-8. (关于状态持久化与故障恢复)

  12. Sokolova, M., & Lapalme, G. (2009). A systematic analysis of performance measures for classification tasks. Information Processing & Management, 45(4), 427-437. DOI: 10.1016/j.ipm.2009.03.002 (用于状态管理性能度量)

  13. Wing, J. M. (2006). Computational thinking. Communications of the ACM, 49(3), 33-35. DOI: 10.1145/1118178.1118215 (关于状态机思维)

  14. Beck, K. (2002). Test-Driven Development: By Example. Addison-Wesley. ISBN: 978-0-32114-653-3. (关于状态管理的测试驱动开发)

  15. Evans, E. (2003). Domain-Driven Design: Tackling Complexity in the Heart of Software. Addison-Wesley. ISBN: 978-0-32112-521-7. (关于领域切片设计)

延伸阅读

附录 A:Redux Toolkit 完整模板

// 完整的 Redux Toolkit 项目模板
import { configureStore, createSlice, createAsyncThunk, createEntityAdapter } from '@reduxjs/toolkit';
import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux';

// 1. 定义 Slice
const todosAdapter = createEntityAdapter<Todo>();
const initialState = todosAdapter.getInitialState({ loading: false, error: null as string | null });

export const fetchTodos = createAsyncThunk('todos/fetchAll', async () => {
  const res = await fetch('/api/todos');
  return res.json();
});

const todosSlice = createSlice({
  name: 'todos',
  initialState,
  reducers: {
    addTodo: todosAdapter.addOne,
    toggleTodo: (state, action) => {
      const todo = state.entities[action.payload];
      if (todo) todo.completed = !todo.completed;
    },
  },
  extraReducers: (builder) => {
    builder
      .addCase(fetchTodos.pending, (state) => { state.loading = true; })
      .addCase(fetchTodos.fulfilled, (state, action) => {
        todosAdapter.setAll(state, action.payload);
        state.loading = false;
      });
  },
});

// 2. 配置 Store
const store = configureStore({
  reducer: { todos: todosSlice.reducer },
});

// 3. 导出类型
type RootState = ReturnType<typeof store.getState>;
type AppDispatch = typeof store.dispatch;
const useAppDispatch = () => useDispatch<AppDispatch>();
const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;

附录 B:TypeScript 高级技巧

// 1. 状态类型自动推导
const slice = createSlice({
  name: 'counter',
  initialState: { count: 0 },
  reducers: {
    increment: (state) => { state.count += 1; },
    add: (state, action: PayloadAction<number>) => { state.count += action.payload; },
  },
});
// actions.add 的类型自动推导为 (payload: number) => { type: string; payload: number }

// 2. 严格的状态类型
type ReadonlyState<T> = {
  readonly [K in keyof T]: T[K] extends object ? ReadonlyState<T[K]> : T[K];
};

// 3. Reducer 的类型约束
type Reducer<S, A> = (state: S, action: A) => S;
type ActionMap<R extends Record<string, (state: any, action: any) => any>> = {
  [K in keyof R]: R[K] extends (state: any, action: infer A) => any ? A : never;
};

附录 C:性能优化清单

  • 使用 useSelector 的选择器,避免订阅整个状态。
  • 选择器返回新对象时,使用记忆化或浅比较。
  • Reducer 内的数组操作使用不可变方式([...arr, item] 而非 arr.push(item))。
  • 深层嵌套状态使用 immer 简化更新。
  • 大型列表使用 createEntityAdapter 规范化存储。
  • 服务端状态用 React Query 分离,不放入全局 Store。
  • 临时 UI 状态用 useState,不放入全局 Store。
  • 开发环境启用 DevTools,生产环境关闭或脱敏。
  • 持久化仅保留必要字段,避免敏感数据泄漏。
  • 使用 React.memo 包裹纯展示组件,避免父组件重渲染波及。

附录 D:调试技巧

// 1. 订阅调试:监听所有状态变化
store.subscribe(() => {
  console.log('State:', store.getState());
});

// 2. 选择器调试:记录计算耗时
const selectExpensive = createSelector(
  [selectItems],
  (items) => {
    const start = performance.now();
    const result = items.filter(/* ... */);
    console.log(`selectExpensive 耗时: ${performance.now() - start}ms`);
    return result;
  },
);

// 3. 中间件调试:记录 dispatch 链
const debugMiddleware = ({ getState }) => (next) => (action) => {
  console.trace('Dispatch:', action.type);
  return next(action);
};

// 4. 为什么重渲染:使用 why-did-you-render
if (process.env.NODE_ENV === 'development') {
  const whyDidYouRender = require('@welldone-software/why-did-you-render');
  whyDidYouRender(React, { trackAllPureComponents: true });
}

附录 E:状态归一化(Normalization)

// 归一化:将嵌套数据拍平为字典,O(1) 查找
import { createEntityAdapter, EntityState } from '@reduxjs/toolkit';

interface Post {
  id: string;
  title: string;
  authorId: string;
  comments: Array<{ id: string; text: string }>;
}

// 归一化前:嵌套结构,查找需遍历
const nestedPosts: Post[] = [
  { id: '1', title: 'A', authorId: 'u1', comments: [{ id: 'c1', text: 'Good' }] },
];

// 归一化后:分表存储
interface NormalizedState {
  posts: EntityState<Post>;
  comments: EntityState<{ id: string; text: string; postId: string }>;
}

const postsAdapter = createEntityAdapter<Post>();
const commentsAdapter = createEntityAdapter<{ id: string; text: string; postId: string }>();

// 查找:O(1) 而非 O(n)
const selectPost = (state: NormalizedState, id: string) =>
  postsAdapter.getSelectors().selectById(state.posts, id);

const selectCommentsByPost = (state: NormalizedState, postId: string) =>
  commentsAdapter.getSelectors().selectAll(state.comments).filter((c) => c.postId === postId);

附录 F:Signal 响应式实现

// 极简 Signal 实现:理解响应式原理
let currentEffect: (() => void) | null = null;

class Signal<T> {
  private value: T;
  private subscribers = new Set<() => void>();

  constructor(initial: T) {
    this.value = initial;
  }

  get(): T {
    // 读取时若有正在执行的 effect,建立依赖
    if (currentEffect) {
      this.subscribers.add(currentEffect);
    }
    return this.value;
  }

  set(v: T): void {
    if (Object.is(v, this.value)) return;
    this.value = v;
    // 通知所有订阅者
    this.subscribers.forEach((fn) => fn());
  }
}

function effect(fn: () => void): () => void {
  const run = () => {
    currentEffect = run;
    fn();
    currentEffect = null;
  };
  run();
  return () => { /* 清理依赖 */ };
}

// 使用
const count = new Signal(0);
const double = new Signal(0);

effect(() => {
  double.set(count.get() * 2);
  console.log('Double:', double.get());
});

count.set(5);  // 输出: Double: 10
count.set(10); // 输出: Double: 20

附录 G:跨框架状态共享

// 跨框架状态共享:基于 Proxy 的响应式对象
import { proxy, subscribe } from 'valtio';

// 框架无关的状态对象
const sharedState = proxy({
  count: 0,
  user: { name: 'Alice' },
});

// React 中使用
import { useSnapshot } from 'valtio/react';
function ReactComponent() {
  const snap = useSnapshot(sharedState);
  return <div>{snap.count}</div>;
}

// Vue 中使用
import { watchEffect } from 'vue';
function VueComponent() {
  watchEffect(() => {
    console.log(sharedState.count);
  });
}

// 原生 JS 中使用
subscribe(sharedState, () => {
  console.log('State changed:', sharedState);
});

// 任何框架都可读写
sharedState.count += 1;

附录 H:状态机可视化

// XState 状态图可视化
import { createMachine } from 'xstate';
import { visualize } from '@xstate/inspect';

const machine = createMachine({
  id: 'traffic',
  initial: 'green',
  states: {
    green: { after: { 5000: 'yellow' } },
    yellow: { after: { 1000: 'red' } },
    red: { after: { 5000: 'green' } },
  },
});

// 打开可视化调试器
// inspect({ url: 'https://stately.ai/viz' });

附录 I:测试工具集

// 状态管理测试工具
import { renderHook, act } from '@testing-library/react-hooks';

// 测试 Zustand store
test('store 应正确更新', () => {
  const { result } = renderHook(() => useBearStore());
  expect(result.current.bears).toBe(0);
  act(() => result.current.increase(1));
  expect(result.current.bears).toBe(1);
});

// 测试 XState
import { interpret } from 'xstate';
test('状态机应正确转移', (done) => {
  const service = interpret(gameMachine).onTransition((state) => {
    if (state.matches('victory')) done();
  });
  service.start();
  service.send({ type: 'START' });
  service.send({ type: 'PLAYER_ATTACK', damage: 100 });
});

// Mock fetch 测试 async thunk
global.fetch = jest.fn(() =>
  Promise.resolve({ ok: true, json: () => Promise.resolve({ id: '1', name: 'Test' }) }),
) as any;
返回入门指南