mcpField
Z
zustand

zustand

github.com/lobehub开发83.6k6.9k 次安装今天更新
代码仓库

关于 zustand

面向 LobeHub 开发的 Zustand 状态管理技能,说明项目中的存储约定。内容涵盖仅存状态的 store、基于 replica 的服务端数据管理,以及编辑或迁移旧版 store 的规则。

主要功能

  1. 01定义仅存状态的 store 结构
  2. 02区分公开与私有 action
  3. 03使用 replica 管理服务端数据
  4. 04处理水合与乐观更新
  5. 05遵循旧版 store 约定

适用场景

  1. 01创建或迁移 Zustand store
  2. 02添加基于 replica 的列表和详情数据
  3. 03实现带回滚的乐观写入

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/lobehub/lobehub --skill zustand

安装后在 Claude Code 中输入/zustand确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

references/
data-structures.md
legacy-slices.md
references/data-structures/
reducer.md
types.md

详细介绍

LobeHub Zustand 状态管理

状态结构和类型

  • 从 @lobechat/types 导入共享的 store 类型,而不是从 @lobechat/database 导入。
  • 将轻量列表项类型与完整详情类型分开;列表类型不得扩展较重的详情类型。
  • 按 ID 缓存详情(xxxDetailMap: Record<string, Detail>);不要持有单个 currentDetail 对象。
  • 通过 xxxSelectors 聚合选择器(selectors.ts)读取状态,不要在各个组件中重复编写临时 lambda。
  • 在选择列表/详情结构或类型来源之前,请先阅读 数据结构。

仅含状态的 Store(新建和迁移的 Store 默认采用此方式)

操作不放在 Zustand 状态中。参考:src/store/page。

文件负责内容
store.tsuseXStore = createWithEqualityFn<XState>()(devtools(() => initialState), shallow):仅含状态,不含方法
privateAction.tsXPrivateAction:供公共操作共享的副本切片、实体链接和写入辅助方法。不从 index.ts 导出
action.tsXActionImpl(公共 API)以及单例 xActions = new XActionImpl(setState, getState, new XPrivateAction(…))
  • 组件使用 useXStore(selector) 读取状态,并直接调用 xActions.foo()。不要从 Store 中选择操作。
  • 同步钩子放在公共类上,并通过 xActions.useFetchX(params) 调用。
  • 公共操作通过 this.foo() 相互调用。通过 this.#private 访问私有辅助方法。不要在 Store 中添加 internal_* 方法来供不同类共享逻辑。
  • 为每次写入标注名称:this.#set(partial, false, n('actionName')),其中 const n = setNamespace('x')。
  • 在公共类上添加 reset(),并在 src/store/utils/userDataStores.ts 中将 xActions 注册到 resetableActions。
  • 测试通过 xActions 执行操作,并断言 useXStore.getState()。在组件测试中进行模拟:vi.mock('@/store/x', () => ({ xActions: { foo: vi.fn() }, useXStore: … }))。

副本驱动的服务端数据

UI 渲染的服务端列表和详情通过 @/libs/replica 处理,而不是手写 SWR onSuccess + reducer。引擎负责从 IndexedDB 中恢复数据、重新验证最新数据、分页、带回滚的乐观更新,以及作用域(用户 + 工作区)隔离。API 参考:local-replica README。引擎在 lobehub/local-replica 中维护:在那里修复、发布,然后在此处升级依赖。

  1. 在 store 的 projection.ts 中定义资源(defineReplica / definePagedReplica),在边界处转换服务端行数据。
  2. 为每个副本添加两个状态字段:视图(xxxMap: Record<key, TData>)及其簿记状态(xxxReplica: createReplicaState())。
  3. 在私有类中绑定:createReplicaSlice(resource, { get, set, stateKey: 'xxxReplica', view: recordLens('xxxMap'), … })。同一实体存在于多个副本中(列表行 + 详情)时,使用 linkReplicaEntity([list, detail]) 将它们关联起来。
  4. 读取:slice.useSync(params) 仅返回获取状态标志(isHydrated、isValidating、error、revalidate)。数据通过 xxxMap 上的选择器获取。“条目为 undefined”表示正在加载——不要添加 xxxInit 标志或 loadingXxxIds。
  5. 在没有挂载视图时读取(悬停预取、变更后刷新):await slice.fetch(params)(写入后使用 { fresh: true })。绝不要手动调用服务并执行 slice.replace(...),因为这样会跳过作用域、移除、清除和过时请求保护。每个响应的副作用(未找到映射、为另一个 store 写入初始数据)都应放在切片的 effects(onApplied / onMissing / onPurge / onError)中,并使用 isMissing,而不是放在某个 hook 的 onSuccess 中。
  6. 无骨架屏的冷启动:useSync 仅在组件挂载后才会恢复数据。对于必须从本地副本渲染的列表,添加一个 preHydrate action(ensureScope + hydrate),并通过惰性包装器(src/spa/router/pageListLoader.ts、agentChatTopicListLoader.ts)从路由的 loader 中调用它,再通过 @/libs/replica/prePaint 中的 settleWithin(…, PRE_PAINT_HYDRATE_TIMEOUT) 限制等待时间。@/libs/replica 预设了缓存作用域、IndexedDB / localStorage 存储(@/libs/replica/storage 中的 AppIndexedDBStorage / AppLocalStorage)以及应用的 SWR 驱动;软件包 README 则保持与应用无关。
  7. 写入:使用 slice.optimistic(key, apply, serverCall) 或 entity.optimistic(id, fn | 'remove', serverCall)。调用失败时会回滚到已确认的值,因此删除也采用乐观更新。仅存在于客户端的行(临时 ID)通过 isClientOnly 声明,并使用 update(key, fn, { persist: false }) 写入初始数据。

除测试数据初始化外,只能通过副本的切片写入其视图。直接使用 set 会绕过引擎的簿记逻辑(持久化、回滚基准值、实体关联)。

旧版 Store

仍将 action 类扁平化到状态中的 Store(flattenActions、create*Slice、internal_*、internal_dispatch* + reducers、通过 SWR onSuccess 同步)遵循旧版切片。只有在编辑这类 Store 时才使用该布局;将其数据迁移到副本时,改用上述布局。

评论

支持 Markdown