architecture adr ADR 003 dual state management - WangYajun369/ai-writing-platform GitHub Wiki
ADR-003:Zustand 管业务状态 + Jotai 管 UI 瞬态
状态:已采纳(v1.6.0 演进:slice 组合阶段已被三个独立领域 store 取代,见下「后续演进」;「Zustand 管业务 + Jotai 管 UI」的决策结论仍有效) 日期:2026-06-03(v0.1.0) 影响范围:前端
背景
编辑器场景的状态特征差异极大:
| 状态类型 | 例子 | 特征 |
|---|---|---|
| 业务数据 | 书籍、章节、卷、AI 配置 | 跨页面共享、需持久化、变更频率低 |
| UI 瞬态 | 面板开关、保存中标记、光标位置、滚动位置 | 高频变更(流式期间每秒数十次)、多为单窗口内使用 |
| 独立窗口开关 | 世界观/历史/工具箱/调试窗口 | 需跨窗口共享,但不持久化 |
单一方案难以同时满足。
备选方案
| 方案 | 优点 | 缺点 |
|---|---|---|
| A. 单一 Redux Toolkit | 生态成熟、DevTools 强大 | 样板代码多、高频更新性能差、小状态也要走 action |
| B. 单一 Zustand | 简洁、性能好 | 高频 UI 状态写入 store 会触发大范围订阅者重渲染 |
| C. 单一 Jotai | 原子级订阅、细粒度重渲染 | 缺乏中间件生态、持久化需自行处理 |
| D. Zustand(业务)+ Jotai(UI) | 各取所长 | 两套心智模型,需要明确分工约定 |
决策
采用方案 D 双轨制,并确立分工原则:
- 跨页面共享且需持久化 → Zustand(slice 模式:
booksSlice/aiSlice/preferencesSlice/pluginStore) - 单窗口内高频变化的瞬态 → Jotai(21 个 atom,含 5 个独立窗口开关)
- 仅单个组件树使用 → 局部 Hook(如
useAgent)
理由
- 细粒度订阅:Jotai 的原子级订阅让「保存中」「字数」等高频状态变更只重渲染依赖它的最小组件,避免 Zustand 单 store 的广播式通知
- 持久化简单:Zustand 可直接在 action 中写 localStorage;UI 瞬态本就不需要持久化
- 跨窗口共享:Jotai atom 天然支持跨窗口同步独立窗口开关状态(
worldWindowOpenAtom等) - 迁移成本:早期为单一
appStore,v1.0.0 重构为 slice 模式,逐步向独立 store 演进
后果
正面
- 流式 AI 输出的每秒数十次更新不会引发大范围重渲染
- 业务状态与 UI 状态职责清晰,易于定位问题
负面 / 代价
- 两套心智模型,新人需要理解分工约定
- slice 仍是代码组织层面的拆分,未实现真正的状态隔离:
useAppStore()的订阅粒度问题依然存在 - 类型定义(
appTypes.ts)仍较庞大
需要 follow-up 的事项
将 slice 升级为真正独立的 store(✅ v1.6.0 已落地(booksStore/aiStore/preferencesStore)appStore.ts保留为出口 + 跨域选择器,见 状态管理)✅ v1.6.0 已拆为useAiChat(483 行)职责过多hooks/useAgentChatStream/hooks/useAiChatMessages/hooks/useConversationSummarizer+ 主编排(原建议 4 hook,落地形态见 优化报告 问题 6)- 流式期间高频更新不写 localStorage(仅内存),流结束后一次性持久化 —— v1.6.0 演进为 800ms 防抖合并写盘 + 卸载 flush 兜底(问题 4),需保持
后续演进(v1.6.0):本 ADR 采纳时的 slice 组合阶段已结束。业务状态拆分为三个互不持有对方状态的领域 store(
stores/booksStore.ts/stores/aiStore.ts/stores/preferencesStore.ts),appStore.ts仅做再导出与跨域便捷选择器,订阅粒度问题由独立 store 各自收敛解决。