类型安全的状态管理
构建类型安全的状态管理系统,涵盖 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 的关键约束:
- Action 是唯一的状态变更入口,必须显式 dispatch。
- Store 是状态的唯一持有者,不可直接修改,只能通过注册的回调响应 Action。
- View 只能读取 Store,变更必须通过 Action 发起。
这一架构解决了双向绑定的环路问题,但 Flux 原始实现复杂、Store 之间依赖管理困难。社区很快推出了简化版本。
Redux 的简化与普及
2015 年,Dan Abramov 发布 Redux,将 Flux 简化为三条原则:
- 单一数据源:整个应用的状态存储在一个对象树中。
- 状态只读:改变状态的唯一方式是 dispatch 一个 action。
- 纯函数变更: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 的 Action:
switch (action.type)模式下,编译器自动缩窄 payload 类型。 - 派生类型的 Selector:选择器返回类型由输入类型推导,无需手动标注。
- 中间件的类型推导:
thunk、saga等中间件的参数类型自动推导。 - DevTools 的类型提示:Action 历史与状态快照在 DevTools 中有类型信息。
然而,TypeScript 的类型系统在状态管理中也面临挑战:深层嵌套的不可变更新类型、跨切片的依赖推导、运行时校验与编译期类型的一致性。本节将系统性地解决这些挑战。
形式化定义
状态与 Store 的形式化
设 为应用所有可能状态的集合, 为某一时刻的状态。Store 是一个三元组:
其中:
- :当前状态(私有,外部不可直接访问)。
- :返回当前状态的快照。
- :更新状态。
关键约束:状态不可变。即 不修改 ,而是创建新的 并替换:
Action 与 Reducer 的形式化
Action 是描述状态变更意图的对象,形式化为 tagged union:
其中 是字面量类型(如 'increment'、'setUser'), 是对应的 payload 类型。Discriminated Union 保证 switch (action.type) 时编译器能缩窄类型。
Reducer 是纯函数:
必须满足:
- 纯函数: 不产生副作用,相同输入相同输出。
- 不可变:返回新状态,不修改 。
- 默认返回:未知 action 返回原状态:。
单向数据流的形式化
Redux 的单向数据流可形式化为状态转换系统:
其中 是 时刻状态, 是 时刻 dispatch 的 action。整个状态历史是一个序列:
给定初始状态 与 action 序列 ,可以完整重建任意时刻的状态。这是时间旅行调试的数学基础:
Selector 与记忆化的形式化
Selector 是从状态派生值的纯函数:
其中 是派生值的类型。记忆化(Memoization)通过缓存上次的输入与输出避免重复计算:
更精细的策略是输入相等性检查而非引用相等:
reselect 库的核心即此。默认 isEqual 为 ===,可替换为 shallowEqual 或 deepEqual。
中间件的形式化
中间件是”包裹” dispatch 的高阶函数,形式化为洋葱模型:
每个中间件 接收 next(下一个 dispatch)与 action,可决定是否调用 next(action)、修改 action、或完全拦截:
常见中间件职责:
- logger:记录 action 与状态变化。
- thunk:允许 action 是函数,支持异步逻辑。
- saga:基于 Generator 的复杂异步流。
- persist:将状态持久化到 localStorage/IndexedDB。
- devtools:与 Redux DevTools 通信。
有限状态机的形式化
有限状态机(FSM)是五元组:
其中:
- :有限状态集。
- :有限输入集(事件)。
- :状态转移函数。
- :初始状态。
- :终止状态集(前端场景通常为空)。
FSM 的核心价值是非法状态不可表达。例如,一个按钮的”加载中”状态不应与”禁用”状态同时存在——FSM 通过显式建模 idle | loading | success | error 四态,避免组合爆炸。
XState 将 FSM 扩展为状态图(Statechart),支持层次化状态、并行状态、守卫条件、副作用,由 Harel Statecharts 理论支撑。
理论推导
推导一:为何 Reducer 必须是纯函数
Redux 的核心约束是 reducer 纯函数。这一约束的必要性来自三个层面:
1. 可预测性
设 reducer 为 。若 是纯函数,则:
这意味着给定相同状态与 action,结果必然相同。开发者无需考虑”这次调用会不会因为副作用产生不同结果”。
2. 可序列化
纯函数约束使状态历史可序列化为 。这一序列可:
- 保存到文件,供后续回放。
- 通过网络传输,实现协作编辑。
- 用于自动化测试,只需记录输入序列即可复现任意 Bug。
若 reducer 有副作用(如发起网络请求、修改全局变量),则序列化丢失了关键信息,回放结果会偏离。
3. 时间旅行可行性
时间旅行要求”从 任意跳转到 “。若 reducer 纯,则:
这一计算无副作用,可任意重复执行。若 reducer 不纯,则跳转可能触发副作用(如重复发起请求),导致应用状态混乱。
反例:thunk 中间件为何”打破”纯函数约束
thunk 允许 dispatch 一个函数而非 action 对象,函数内部可包含副作用。这看似违反纯函数原则,实则不然:
- thunk 是中间件层面的能力,reducer 仍是纯函数。
- thunk 函数的副作用发生在 dispatch 之前,reducer 接收的仍是纯 action。
- 副作用与状态变更分离,是 Flux 标准模式(Flux Standard Action)的体现。
推导二:Discriminated Union 的类型缩窄
TypeScript 的 discriminated union 配合 switch 语句实现类型缩窄。形式化地,设:
当 action.type 被匹配为 'A' 时,TypeScript 编译器通过控制流分析将 action 缩窄为 ,从而访问 action.payload 时类型为 。
这一缩窄的可靠性基于:
- 字面量类型的不可变性:
type: 'A'是字面量类型,运行时值严格等于'A'。 - switch 的穷尽性检查:若未覆盖所有 union 成员,
never类型断言会报错。 - 编译期与运行时的一致性:字面量类型在编译后被擦除,但运行时值仍为字符串,
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 的 useSyncExternalStore 与 useSelector 通过引用相等性判断状态是否变化。这要求不可变更新的返回值与原状态引用不同:
但”引用不同”不等于”内容不同”。若 f 实际上返回了与原状态等价的对象(但新引用),会触发不必要的重渲染。反之,若 f 直接修改原状态(mutation),引用不变,React 不会感知变化,UI 不更新——这是最常见的 React Bug 之一。
不可变更新的标准模式是浅拷贝 + 递归:
即沿路径创建新对象,未修改的分支保持原引用(结构共享)。这一策略的时间复杂度为 ,空间复杂度也为 ,远优于深拷贝。
immer 库通过 Proxy 实现”可变 API 写法,不可变结果”:
produce(state, draft => {
draft.user.profile.age += 1; // 看似 mutation
});
// 返回新对象,原 state 不变
推导四:原子状态的依赖图
原子状态(Recoil/Jotai)将状态拆分为最小粒度,通过依赖图自动传播更新。形式化地,原子集合 ,每个原子 有值 。派生原子 依赖若干基础原子:
当 变化时,仅 及其下游依赖被重新计算,其他原子不受影响。这一细粒度更新的复杂度为:
对于大型应用,这一差异可达数量级。Redux 的 useSelector 虽然也支持细粒度订阅,但每次 dispatch 都会触发所有 selector 的运行(即使结果未变),而原子状态仅在依赖变化时才计算。
推导五:Signal 的推拉混合
Signal(Solid、Vue 3、Angular)采用”推拉混合”策略:
- 写入时推送:
signal.set(v)通知所有依赖该 signal 的 effect。 - 读取时拉取:effect 执行时读取 signal 当前值,自动建立依赖关系。
这一策略通过依赖追踪实现:
当 signal.set(v) 时,沿依赖图反向查找所有 effect,标记为”脏”。下次渲染时,仅脏 effect 重新执行。
与 React 的 VDOM diff 相比,Signal 的优势是无需 diff——精确知道哪些 effect 依赖哪些 signal,直接执行。代价是依赖追踪的运行时开销与隐式依赖的调试难度。
复杂度分析
状态管理操作的复杂度:
- getState:,直接返回引用。
- setState(浅合并):,创建新对象。
- setState(immer):,结构共享。
- selector(无记忆化):,每次调用都计算。
- selector(reselect):。
- dispatch(无中间件):。
- dispatch(有中间件):上述 + 。
- 时间旅行跳转:,从头回放或从快照恢复。
对于 1000 个订阅者的大型应用,每次 dispatch 的通知成本为 。若每个订阅者的 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 Context | 0KB | 平缓 | 手动 | 差 | 内置 | 简单主题、用户信息 |
选型建议:
- 小型应用(<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.js或Structura。 - 避免手写深拷贝,性能与正确性都难以保证。
常见陷阱与反模式
陷阱 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 膨胀,难以维护。
- 不必要的全局订阅,性能下降。
- 组件复用性降低(依赖全局状态)。
修复:遵循”状态归属”原则。
- 局部状态(仅当前组件用):
useState、useReducer。 - 共享状态(多个组件用):全局 Store 或 Context。
- URL 状态(需分享/刷新):URL 参数 +
nuqs。 - 服务端状态(来自 API):React Query、SWR。
- 表单状态:
react-hook-form、Formik。
陷阱 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)。
- 扩展点:预留插件机制(如自定义中间件)、状态切片动态加载(微前端场景)。
参考文献
-
Abramov, D., & Clark, A. (2015). Redux: Predictable state container for JavaScript apps. GitHub Repository. https://github.com/reduxjs/redux. DOI: 10.5281/zenodo.4764471
-
Wiggins, A. (2011). The Twelve-Factor App: Methodology for building modern web applications. Heroku. https://12factor.net/
-
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
-
Odersky, M., Spoon, L., & Venners, B. (2019). Programming in Scala (5th ed.). Artima Press. ISBN: 978-0-9815316-4-2. (关于协变与逆变的类型理论)
-
Edwards, J. (2014). Flux: Application architecture for building user interfaces. Facebook Engineering Blog. https://facebook.github.io/flux/
-
Bini, A. (2019). Reactive Programming with RxJS 5. Packt Publishing. ISBN: 978-1-78646-821-0.
-
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
-
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
-
Carlo, D. (2021). Immer: Create the next immutable state by mutating the current one. GitHub. https://github.com/immerjs/immer
-
Davis, M. (2018). XState: State machines and statecharts for the modern web. GitHub. https://github.com/statelyai/xstate
-
Nygard, M. T. (2017). Release It!: Design and Deploy Production-Ready Software (2nd ed.). Pragmatic Bookshelf. ISBN: 978-1-68050-239-8. (关于状态持久化与故障恢复)
-
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 (用于状态管理性能度量)
-
Wing, J. M. (2006). Computational thinking. Communications of the ACM, 49(3), 33-35. DOI: 10.1145/1118178.1118215 (关于状态机思维)
-
Beck, K. (2002). Test-Driven Development: By Example. Addison-Wesley. ISBN: 978-0-32114-653-3. (关于状态管理的测试驱动开发)
-
Evans, E. (2003). Domain-Driven Design: Tackling Complexity in the Heart of Software. Addison-Wesley. ISBN: 978-0-32112-521-7. (关于领域切片设计)
延伸阅读
- Redux 官方文档:https://redux.js.org/ - 完整的 Redux 教程与最佳实践。
- Redux Toolkit 文档:https://redux-toolkit.js.org/ - 现代 Redux 推荐方案。
- Zustand GitHub:https://github.com/pmndrs/zustand - 极简状态管理库。
- Jotai 文档:https://jotai.org/ - 原子状态管理。
- XState 文档:https://xstate.js.org/docs/ - 有限状态机与状态图。
- React Query 文档:https://tanstack.com/query/ - 服务端状态管理。
- Immer 文档:https://immerjs.github.io/immer/ - 不可变更新简化。
- Reselect:https://github.com/reduxjs/reselect - 记忆化选择器库。
- MobX 文档:https://mobx.js.org/ - 响应式状态管理。
- Valtio 文档:https://github.com/pmndrs/valtio - 代理状态管理。
- Flux Standard Action:https://github.com/redux-utilities/flux-standard-action - Action 规范。
- Harel Statecharts 论文:https://www.wisdom.weizmann.ac.il/~harel/papers/Statecharts.pdf - 状态图理论。
- CRDT 论文:https://hal.inria.fr/inria-00555588/document - 无冲突复制数据类型。
- React useSyncExternalStore RFC:https://github.com/reactjs/rfcs/blob/main/text/0214-use-sync-external-store.md - 外部 Store 接入标准。
- Solid Signals 介绍:https://www.solidjs.com/docs/latest#createsignal - 细粒度响应式。
附录 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;