阶段 3 | 状态管理¶
目标:理解 Claude Code 的状态管理方案 —— 一个 60 行自研 store + 一个巨型 AppState 数据模型 + 一组 onChange 副作用。 时长:1~2 天 前端类比:从 Redux 时代走过来的人会一眼认出这是 Zustand v0 时代的 API(getState/setState/subscribe 三件套),用
Object.is做引用相等判断。规模上去后做了 selector 优化。
3.1 三件套总览¶
src/state/
├── store.ts 60 行 通用 Store 工厂(getState/setState/subscribe)
├── AppStateStore.ts 569 行 AppState 类型 + 工厂方法(state factory)
├── AppState.tsx 199 行 React Context Provider + useAppState hook
├── selectors.ts ~100 行 纯函数:从 AppState 派生数据
├── onChangeAppState.ts ~80 行 变更时触发的副作用(保存、通知、缓存清理)
└── teammateViewHelpers.ts ~30 行 多 agent 模式的辅助函数
核心抽象:store.ts 是与业务完全解耦的 60 行 store 工厂,和 Zustand 几乎一模一样。整个项目的状态管理就建立在这 60 行上。
3.2 60 行核心:src/state/store.ts¶
// src/state/store.ts (完整 60 行)
type Listener = () => void
type OnChange<T> = (args: { newState: T; oldState: T }) => void
export type Store<T> = {
getState: () => T
setState: (updater: (prev: T) => T) => void
subscribe: (listener: Listener) => () => void
}
export function createStore<T>(
initialState: T,
onChange?: OnChange<T>,
): Store<T> {
let state = initialState
const listeners = new Set<Listener>()
return {
getState: () => state,
setState: (updater: (prev: T) => T) => {
const prev = state
const next = updater(prev)
if (Object.is(next, prev)) return // ← 关键:引用相等则跳过
state = next
onChange?.({ newState: next, oldState: prev })
for (const listener of listeners) listener()
},
subscribe: (listener: Listener) => {
listeners.add(listener)
return () => listeners.delete(listener)
},
}
}
3.2.1 设计要点¶
-
Object.is引用相等 —— Zustand 也是这套。setState时如果updater返回的next跟prev引用相同,不通知 listeners、不触发 onChange。这是性能优化的核心。 -
onChange同步触发 ——onChange在setState内同步调用(在通知 listeners 之前),用于: - 持久化到 globalConfig
- 清理缓存
- 通知外部(IDE、bridge)
⚠️ 注意:listeners 在 onChange 之后才被调用,保证 listeners 看到的是新 state。
-
返回 unsubscribe 函数 ——
subscribe(listener)返回() => listeners.delete(listener),经典 cleanup pattern,和 ReactuseEffectcleanup 同形。 -
没有
dispatch/action概念 —— 这是 Zustand 风格(无 action 类型),不是 Redux 风格(action + reducer)。
3.2.2 跟 Zustand 的对比¶
| 特性 | Claude Code | Zustand v4 |
|---|---|---|
| API 形态 | getState/setState/subscribe |
getState/setState/subscribe |
| 引用相等 | Object.is |
Object.is |
| 中间件 | onChange 单一钩子 |
subscribeWithSelector / persist / devtools ... |
| 异步 action | 自己写(业务层) | 自己写(业务层) |
| TypeScript | 泛型 <T> |
泛型 <T> |
| 生态 | 0(自研) | 巨大 |
结论:Claude Code 的状态管理 = Zustand 的核心 + 0 中间件。如果你用过 Zustand,60 行就能完全理解。
💡 学习技巧:读
store.ts时手抄一遍 60 行,然后合上文件独立写出来。写不出来说明没真懂。
3.3 巨型状态对象:src/state/AppStateStore.ts¶
文件长度:569 行 角色:定义
AppState类型 + 工厂函数createAppState()返回初始 state
3.3.1 AppState 字段分类¶
AppState 是个扁平大对象(不是嵌套的 state tree),包含 40+ 字段。分类:
| 类别 | 字段示例 | 来源文件 |
|---|---|---|
| 会话元数据 | sessionId, sessionTitle, isResume, originalCwd, projectRoot |
bootstrap/state.ts |
| 消息 | messages: Message[], toolUseSummaries |
types/message.ts |
| 任务 | tasks: Record<TaskId, TaskState> |
tasks/types.ts |
| 工具状态 | toolPermissionContext, denialTracking, toolUseContext |
Tool.ts, utils/permissions/... |
| MCP | mcpServers: Record<string, MCPServerConnection> |
services/mcp/types.ts |
| 设置 | settings: SettingsJson, model, effort, fastMode |
utils/settings/... |
| 权限 | permissionMode, permissionSavingMode, autoCompactTracking |
utils/permissions/... |
| 多 agent | viewingAgentTaskId, teammates, swarm |
tasks/InProcessTeammateTask/... |
| 插件 | loadedPlugins: LoadedPlugin[], pluginErrors |
types/plugin.ts |
| 通知/UI | notifications: Notification[], costState, lastInteractionTime |
context/notifications.js, cost-tracker.ts |
| 桥接 | bridgePermissionCallbacks, channelPermissionCallbacks |
bridge/..., services/mcp/... |
| 生命周期 | eligibility, attributionState, completionBoundary |
utils/... |
| 模式标志 | isBriefOnly, isUltraplanMode, isAutoCompactEnabled |
query.ts, utils/compact/... |
3.3.2 工厂模式 + immer-like 更新¶
虽然文件 569 行,但只有少量 mutation 方法(不像 Redux 有大量 reducer)。绝大多数更新通过 store.setState(prev => ({ ...prev, foo: bar })) 完成 —— 手动 spread 模式。
观察 AppStateStore.ts 里的 createAppState() 工厂函数,返回的是初始值,具体 mutation 散落在调用方。
3.3.3 类型导入的"门面"角色¶
注意 AppStateStore.ts 头部大量 import type 来自:
- services/mcp/types.ts
- tools/AgentTool/...
- utils/permissions/...
- tasks/...
- bridge/...
- services/PromptSuggestion/...
这意味着 AppState 是项目里 90% 类型的"中央集线器"。要理解 Claude Code 的数据模型,先读 AppStateStore.ts 的 import 列表(仅看类型导入即可)。
💡 避免循环依赖的设计:注释里反复出现 "Import from centralized location to break import cycles" / "Inlined from framework.ts — importing creates a cycle"。这告诉我们项目里类型依赖关系有向无环图的设计纪律。要打破循环,就把类型抽到独立文件。
3.4 React 集成层:src/state/AppState.tsx¶
文件长度:199 行 角色:把
Store<AppState>桥接到 React 组件树
// 推测结构(基于 React 模式)
import { createContext, useContext, useEffect, useRef, useSyncExternalStore } from 'react';
const StoreContext = createContext<Store<AppState> | null>(null);
export function AppStateProvider({ children, store }) {
return <StoreContext.Provider value={store}>{children}</StoreContext.Provider>;
}
export function useAppState<T>(selector: (state: AppState) => T): T {
const store = useContext(StoreContext);
return useSyncExternalStore(
store.subscribe,
() => selector(store.getState()),
);
}
3.4.1 关键设计:useSyncExternalStore¶
React 18 提供的 useSyncExternalStore 是把外部 store 接入 React 树的官方推荐 API。Claude Code 用它做 store 订阅,避免自己手写 useState + useEffect + subscribe 模板代码。
💡 如果你的项目用了 Zustand/Jotai/Redux 之外的库,可以把
useSyncExternalStore当通用桥接方案学。
3.4.2 selector 模式¶
// 用法示例(推测)
const messages = useAppState(s => s.messages);
const currentModel = useAppState(s => s.settings.model);
selector 的好处:组件只订阅自己关心的字段,Object.is 引用相等保证不重渲染。
3.5 选择器层:src/state/selectors.ts¶
文件角色:纯函数,从 AppState 派生计算数据 设计原则(来自文件头注释):"Keep selectors pure and simple - just data extraction, no side effects."
// src/state/selectors.ts 头部
import type { InProcessTeammateTaskState } from '../tasks/InProcessTeammateTask/types.js';
import { isInProcessTeammateTask } from '../tasks/InProcessTeammateTask/types.js';
import type { LocalAgentTaskState } from '../tasks/LocalAgentTask/LocalAgentTask.js';
import type { AppState } from './AppStateStore.js';
/**
* Get the currently viewed teammate task, if any.
*/
export function getViewedTeammateTask(
appState: Pick<AppState, 'viewingAgentTaskId' | 'tasks'>,
): InProcessTeammateTaskState | undefined {
const { viewingAgentTaskId, tasks } = appState;
if (!viewingAgentTaskId) return undefined;
const task = tasks[viewingAgentTaskId];
if (!task) return undefined;
if (!isInProcessTeammateTask(task)) return undefined;
return task;
}
3.5.1 Selector 模式的好处¶
- 测试友好 —— 纯函数,输入 AppState、输出派生数据
- 可组合 —— selector 可以调用其他 selector
- 类型安全 ——
Pick<AppState, '...'>显式声明依赖 - 性能可优化 —— 未来加
reselect风格 memoization 不影响调用方
3.5.2 实战:常见 selector 模式¶
// 1. 取单个字段
const isLeader = useAppState(s => s.isLeader);
// 2. 取多字段组合
const { mcpServers, settings } = useAppState(s => ({
mcpServers: s.mcpServers,
settings: s.settings,
})); // ⚠️ 每次都返回新对象,可能触发不必要渲染
// 3. 用 selector 函数
const { mcpServers, settings } = useAppState(useShallow(s => ({
mcpServers: s.mcpServers,
settings: s.settings,
}))); // ✅ 用 shallow equal 优化
💡 第 2 种 vs 第 3 种:第 2 种 selector 每次返回新对象字面量,
Object.is必不等 → 必重渲染。Claude Code 可能用useShallow(zustand 同款)或自研的浅比较 hook 解决。
3.6 副作用编排:src/state/onChangeAppState.ts¶
文件角色:当
AppState变化时,同步执行的副作用(持久化、通知、缓存清理)
3.6.1 典型副作用类型¶
观察 onChangeAppState.ts 头部 import:
| 副作用 | 用途 |
|---|---|
setMainLoopModelOverride |
主 LLM 模型变更时持久化到 config |
clearApiKeyHelperCache / clearAwsCredentialsCache / clearGcpCredentialsCache |
Auth 状态变更时清凭据缓存 |
getGlobalConfig / saveGlobalConfig |
持久化到 ~/.claude/ |
applyConfigEnvironmentVariables |
把新 config 注入 process.env(子进程用) |
notifyPermissionModeChanged |
通知 IDE/bridge 权限模式变了 |
notifySessionMetadataChanged |
通知外部会话元数据变了 |
updateSettingsForSource |
把变更写回对应的 settings 文件 |
3.6.2 设计哲学:同步副作用放在 store 层,异步副作用放在 hook 层¶
这是 Claude Code 的一个清晰边界:
- 同步、能立即完成的副作用(保存、通知、清缓存)→ onChangeAppState.ts
- 异步、依赖 IO 的副作用(API 调用、文件读取)→ hooks/use* 里
💡 这条边界值得学习 —— 它解释了"为什么
useEffect里要做的事不能在 onChange 里做"。
3.7 状态生命周期¶
createAppState() 创建初始 state
↓
createStore(initial, onChange) 包装成 store
↓
AppStateProvider 注入 React Context
↓
[组件 mount]
↓
useAppState(selector) 订阅 + 派生
↓
[用户交互] → setState(updater)
↓
[同步] onChange({new, old}) → 持久化/通知/清缓存
↓
[同步] listeners.forEach(l => l())
↓
[组件 re-render] useSyncExternalStore 触发
3.8 跟 Web 状态管理的对应¶
| Claude Code 模式 | Web 等价 |
|---|---|
createStore<T>(initial) |
Zustand create((set) => ...) |
useSyncExternalStore |
Zustand 内部也用这个 |
| selector 函数 | Reselect / Zustand selector |
onChange 钩子 |
Zustand middleware / Redux middleware |
| 扁平大 state | Redux normalized state |
| Context Provider | React Context / Zustand <StoreProvider> |
Object.is 引用相等 |
Zustand Object.is |
3.9 关键洞察¶
3.9.1 不要过度设计¶
60 行核心 + 569 行类型定义 + 199 行 React 桥接 + 100 行 selectors = 900 行做完完整状态管理。
Redux + Redux Toolkit + Reselect 至少 3000 行。
Claude Code 选择了"最小够用"原则。
3.9.2 类型即文档¶
AppState 字段散落在 30+ 业务模块,但 AppStateStore.ts 把它们全部 import 进来组成一个对象。这意味着读 AppStateStore.ts 的 import 列表 = 读完整个项目的业务领域。
3.9.3 循环依赖的"inlined X"模式¶
注释里反复出现 "Inlined from framework.ts — importing creates a cycle"。
纪律:当遇到循环依赖时,不引入新的 import,而是把需要的代码复制一份到本文件。
优点:编译时无循环,类型清晰。
缺点:重复代码要"keep in sync"(注释里写了)。
这在大型项目里是常见痛点 —— 学习 Claude Code 的取舍。
3.9.4 持久化策略¶
onChange 里同步写 globalConfig,不依赖 debounce。
意味着每次 setState 都有磁盘 IO 成本。
推测项目对 setState 频率有约束(不滥用),或者有内部 batching(React 18 automatic batching 会合并多次 setState 触发一次 onChange)。
3.10 阅读清单¶
- ✅
src/state/store.ts(60 行全文)—— 手抄一遍 - ✅
src/state/AppStateStore.ts:1-100(imports + 类型定义)—— 看字段全貌 - ✅
src/state/AppState.tsx(199 行全文)—— 看 React 桥接 - 🔍
src/state/selectors.ts(全文)—— 找几个 selector 读懂 - 🔍
src/state/onChangeAppState.ts(全文)—— 看持久化逻辑 - 📌
src/state/teammateViewHelpers.ts(通读)—— 多 agent 模式
3.11 练习任务¶
- 手写一个 60 行 Store —— 完全复刻
store.ts,能getState/setState/subscribe,支持onChange钩子。然后写测试覆盖 5 个边界情况 - 用
useSyncExternalStore接入到 React 组件,验证Object.is跳过重渲染的优化 - 找 3 个典型 selector(
grep -nE "^export function" src/state/selectors.ts),画出输入输出数据流 - 思考:如果让你给这个 store 加"时间旅行调试"(像 Redux DevTools 那样),你会怎么改
onChange钩子?需要新增什么 API?
3.12 下一步¶
进入 阶段 4:组件库与设计系统 —— design-system/ 14 个 TUI 基础组件,是 shadcn/Radix 的 TUI 对应物。