状态管理
自研的极简状态管理方案 —— 基于 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 回调,返回一个包含三个方法的对象。 它是整个状态系统的基石,简洁而强大。
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 工作原理
AppState 结构
涵盖 60+ 字段的集中式状态模型
AppState 是一个包含 60+ 字段的 TypeScript 类型, 使用 DeepImmutable 包裹不可变部分, 涵盖了 CLI 运行的所有状态:设置、任务、MCP 连接、插件、权限、UI 交互等。
AppState 数据模型(核心子域)
📋 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 模式实现精准订阅。组件只在自己关心的状态切片变化时重渲染, 避免了整个状态树变化导致的全量重渲染。
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 }))1setState(updater)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 并更新全局模型覆盖。切换模型后重启会话仍然生效。
💾 视图/配置持久化
expandedView、verbose、tungstenPanelVisible 等变化时, 自动保存到 globalConfig(JSON 文件)。
🔑 认证缓存清理
settings 变化时清空 API Key、 AWS/GCP 凭证缓存,确保环境变量变更立即生效。
onChangeAppState 副作用管道
Selectors 派生状态
从 AppState 中派生计算值的纯函数
Selectors 是纯函数,接收 AppState(或其部分字段)作为输入,返回派生值。 它们不产生副作用,只做数据提取和转换。在 Claude Code 中,Selectors 主要用于输入路由和 任务查找。
// 查找当前正在查看的 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 选择性地将关键字段 持久化到文件系统和外部服务。这种设计避免了序列化/反序列化的性能开销, 同时确保重要配置不会丢失。
持久化流程
📁 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 派生计算值。 五层协作,构成了一个高效、可维护的状态管理方案。