🗃️

状态管理

自研的极简状态管理方案 —— 基于 React useSyncExternalStore 的轻量级 Store,零依赖、类型安全、精准重渲染

核心架构

设计哲学

为什么自研而不是用 Redux / Zustand / Jotai?

Claude Code 选择了 自研轻量状态管理, 而非 Redux、Zustand 等社区方案。核心原因在于 Claude Code 是一个 单页终端应用, 状态集中在单个 AppState 对象中,不需要 Redux 的多 reducer、middleware 生态, 也不需要 Zustand 的独立模块化 store。

极简 API

整个 Store 实现仅 ~30 行代码: getState、setState、subscribe 三个方法,零学习成本。

🎯

精准订阅

基于 useSyncExternalStore + Selector 模式, 组件只在自己关心的字段变化时重渲染。

🔗

响应式副作用

onChangeAppState 回调 自动同步状态到持久化存储、外部服务,类似 Redux middleware 但更轻量。

💡 关键决策:不用 Redux 的原因 —— 单 store、单消费者、无时间旅行需求; 不用 Zustand 的原因 —— 不需要独立模块化 store 和 devtools 生态。 自研方案完美匹配 Claude Code 的实际需求。

createStore 实现

30 行代码实现的完整状态管理核心

createStore<T> 是一个泛型函数, 接收初始状态和可选的 onChange 回调,返回一个包含三个方法的对象。 它是整个状态系统的基石,简洁而强大。

store.ts — 核心实现(完整源码)
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)  // 返回取消订阅函数
    },
  }
}

createStore 工作原理

触发initialStateStore<T>getState()setState()subscribe()state (闭包)onChange?listeners

AppState 结构

涵盖 60+ 字段的集中式状态模型

AppState 是一个包含 60+ 字段的 TypeScript 类型, 使用 DeepImmutable 包裹不可变部分, 涵盖了 CLI 运行的所有状态:设置、任务、MCP 连接、插件、权限、UI 交互等。

AppState 数据模型(核心子域)

AppStatesettingstasksmcppluginstoolPermissionUI 状态REPL BridgeTeam/SwarmSpeculation

📋 DeepImmutable 部分

  • settings — 用户配置
  • verbose — 详细模式
  • mainLoopModel — 当前模型
  • toolPermissionContext — 权限
  • expandedView — 展开视图
  • replBridge* — Bridge 状态

🔧 可变部分(含函数类型)

  • tasks — 后台任务 Map
  • agentNameRegistry — 名称注册
  • mcp.tools — MCP 工具列表
  • sessionHooks — 会话钩子 Map
  • replContext — REPL VM 上下文
  • teamContext — 团队协作状态

React 集成

useAppState Hook 与精准重渲染

Claude Code 利用 React 18 的 useSyncExternalStore API, 配合 Selector 模式实现精准订阅。组件只在自己关心的状态切片变化时重渲染, 避免了整个状态树变化导致的全量重渲染。

AppState.tsx — useAppState Hook(简化)
export function useAppState<T>(
  selector: (state: AppState) => T
): T {
  const store = useAppStore()

  const get = () => {
    const state = store.getState()
    return selector(state)  // 只提取需要的切片
  }

  // useSyncExternalStore 保证:
  // 1. 同步读取避免撕裂 (tearing)
  // 2. 仅当 selector 返回值变化时重渲染 (Object.is 比较)
  return useSyncExternalStore(store.subscribe, get, get)
}

// 使用示例 —— 多个独立订阅
const verbose = useAppState(s => s.verbose)
const model = useAppState(s => s.mainLoopModel)
const { text } = useAppState(s => s.promptSuggestion)

// 仅写入,不订阅 —— 永远不会触发重渲染
const setAppState = useSetAppState()
setAppState(prev => ({ ...prev, verbose: true }))
状态更新 → 组件重渲染流程
typescript
1setState(updater)
步骤 1/6

1. 组件调用 setAppState(prev => ({ ...prev, verbose: true }))

onChangeAppState 响应式更新

状态变更的自动副作用管道

onChangeAppState 是 createStore 的 onChange 回调, 在每次 setState 成功后执行。它通过比较 newState 和 oldState, 对特定字段的变化执行副作用 —— 类似 Redux 的 middleware,但以声明式 diff 模式实现。

🔐 权限模式同步

toolPermissionContext.mode 变化时, 自动通知 CCR (Claude Cloud Runtime) 和 SDK 状态流,确保远程 UI 与 CLI 权限模式一致。 还处理 Ultraplan 模式的特殊逻辑。

🤖 模型切换持久化

mainLoopModel 变化时自动写入 userSettings 并更新全局模型覆盖。切换模型后重启会话仍然生效。

💾 视图/配置持久化

expandedViewverbosetungstenPanelVisible 等变化时, 自动保存到 globalConfig(JSON 文件)。

🔑 认证缓存清理

settings 变化时清空 API Key、 AWS/GCP 凭证缓存,确保环境变量变更立即生效。

onChangeAppState 副作用管道

setState()newState vs oldState权限模式模型切换视图配置认证缓存→ CCR 通知→ SDK 通知→ userSettings→ globalConfig

Selectors 派生状态

从 AppState 中派生计算值的纯函数

Selectors 是纯函数,接收 AppState(或其部分字段)作为输入,返回派生值。 它们不产生副作用,只做数据提取和转换。在 Claude Code 中,Selectors 主要用于输入路由 任务查找

selectors.ts — 核心选择器
// 查找当前正在查看的 teammate 任务
export function getViewedTeammateTask(
  appState: Pick<AppState, 'viewingAgentTaskId' | 'tasks'>,
): InProcessTeammateTaskState | undefined {
  const { viewingAgentTaskId, tasks } = appState
  if (!viewingAgentTaskId) return undefined
  const task = tasks[viewingAgentTaskId]
  if (!task || !isInProcessTeammateTask(task)) return undefined
  return task
}

// 判定用户输入应该路由到哪个 agent
export type ActiveAgentForInput =
  | { type: 'leader' }
  | { type: 'viewed'; task: InProcessTeammateTaskState }
  | { type: 'named_agent'; task: LocalAgentTaskState }

export function getActiveAgentForInput(
  appState: AppState,
): ActiveAgentForInput {
  const viewedTask = getViewedTeammateTask(appState)
  if (viewedTask) return { type: 'viewed', task: viewedTask }

  const { viewingAgentTaskId, tasks } = appState
  if (viewingAgentTaskId) {
    const task = tasks[viewingAgentTaskId]
    if (task?.type === 'local_agent')
      return { type: 'named_agent', task }
  }
  return { type: 'leader' }
}

💡 设计要点:使用 Pick<AppState, ...> 而非完整 AppState 作为参数, 遵循最小依赖原则。Selector 返回的是已有的子对象引用而非新对象, 确保 Object.is 比较能有效避免不必要的重渲染。

持久化策略

内存 → 文件系统 → 外部服务的三级同步

Claude Code 采用 非自动持久化 策略: AppState 本身仅存在于内存中,通过 onChangeAppState 选择性地将关键字段 持久化到文件系统和外部服务。这种设计避免了序列化/反序列化的性能开销, 同时确保重要配置不会丢失。

持久化流程

setStateexpandedView verbosemainLoopModelpermission modeAppState (内存)onChange AppState字段 DiffglobalConfig (JSON 文件)userSettings (JSON 文件)CCR / SDK (网络)

📁 globalConfig.json

存储 verbose、expandedView、tungstenPanelVisible 等 UI 偏好。 通过 saveGlobalConfig 原子写入。

📁 userSettings.json

存储模型选择等用户配置。 通过 updateSettingsForSource 按源合并。

☁️ CCR / SDK

权限模式变更实时推送至 Claude Cloud Runtime 和 SDK 状态流, 确保远程 UI 与 CLI 同步。

架构总结

整体数据流与设计模式

Claude Code 的状态管理是一个精心设计的分层架构:createStore 提供底层存储,AppStateProvider 注入 React 上下文,useAppState 实现精准订阅,onChangeAppState 处理副作用,Selectors 派生计算值。 五层协作,构成了一个高效、可维护的状态管理方案。

状态管理分层架构

React 组件useAppState / useSetAppStateuseSyncExternalStoreStore<AppState>onChangeAppStateSelectorsgetDefaultAppState