跳转至

阶段 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 设计要点

  1. Object.is 引用相等 —— Zustand 也是这套。setState 时如果 updater 返回的 nextprev 引用相同,不通知 listeners、不触发 onChange。这是性能优化的核心。

  2. onChange 同步触发 —— onChangesetState同步调用(在通知 listeners 之前),用于:

  3. 持久化到 globalConfig
  4. 清理缓存
  5. 通知外部(IDE、bridge)

⚠️ 注意:listeners 在 onChange 之后才被调用,保证 listeners 看到的是新 state

  1. 返回 unsubscribe 函数 —— subscribe(listener) 返回 () => listeners.delete(listener),经典 cleanup pattern,和 React useEffect cleanup 同形

  2. 没有 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 模式的好处

  1. 测试友好 —— 纯函数,输入 AppState、输出派生数据
  2. 可组合 —— selector 可以调用其他 selector
  3. 类型安全 —— Pick<AppState, '...'> 显式声明依赖
  4. 性能可优化 —— 未来加 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 阅读清单

  1. src/state/store.ts(60 行全文)—— 手抄一遍
  2. src/state/AppStateStore.ts:1-100(imports + 类型定义)—— 看字段全貌
  3. src/state/AppState.tsx(199 行全文)—— 看 React 桥接
  4. 🔍 src/state/selectors.ts(全文)—— 找几个 selector 读懂
  5. 🔍 src/state/onChangeAppState.ts(全文)—— 看持久化逻辑
  6. 📌 src/state/teammateViewHelpers.ts(通读)—— 多 agent 模式

3.11 练习任务

  1. 手写一个 60 行 Store —— 完全复刻 store.ts,能 getState/setState/subscribe,支持 onChange 钩子。然后写测试覆盖 5 个边界情况
  2. useSyncExternalStore 接入到 React 组件,验证 Object.is 跳过重渲染的优化
  3. 找 3 个典型 selectorgrep -nE "^export function" src/state/selectors.ts),画出输入输出数据流
  4. 思考:如果让你给这个 store 加"时间旅行调试"(像 Redux DevTools 那样),你会怎么改 onChange 钩子?需要新增什么 API?

3.12 下一步

进入 阶段 4:组件库与设计系统 —— design-system/ 14 个 TUI 基础组件,是 shadcn/Radix 的 TUI 对应物。