关键设计决策和原因
本文汇总 Zustand 主要设计取舍:为何简单、为何在并发下仍安全、代价是什么。细节实现见各专题章节。
1. 为什么不需要 Provider?
决策:create() 得到模块级 store + Hook,无 Context Provider。
| 收益 | 代价 |
|---|---|
API 少一层包裹、任意组件直接 useStore | 默认单例,难以「同页两套 store」除非多 create 或自建 Context |
| 避免 Context value 变化导致大范围重渲染 | 测试需 mock 模块或注入 Vanilla store |
性能:订阅走 store 的 Set,不是 Context 自上而下传播;配合选择器只更新相关组件(07-性能优化)。
需要多实例时:createStore + React Context 传 api,或按路由/租户拆分模块。
2. 为什么使用 useSyncExternalStore?
决策:React 集成仅用官方外部 store Hook,不用 useState + useEffect 订阅。
原因:并发下 tearing、僵尸子组件、SSR 快照不一致(06-并发)。
权衡:要求 React 18+(或 backport 包);行为与 React 调度绑定,自定义订阅需理解 snapshot 语义。
3. 为什么默认浅合并?
决策:set(partial) 顶层 Object.assign,非深合并。
| 收益 | 代价 |
|---|---|
| 实现小、语义可预测、性能好 | 嵌套对象需手动展开或 Immer |
与 Redux reducer 常返回新顶层对象的习惯一致;深合并易引发「意外保留旧嵌套字段」与性能问题。
4. 为什么选择「推荐单一 Store」而非强制原子化?
决策:库不强制 Jotai/Recoil 式原子,但推荐按领域组织一个或少量 store。
| 单一 / 少量 store | 多 store / slices |
|---|---|
| 简单应用、全局 UI+用户 | 大应用、按域拆分 bundle 与订阅 |
| actions 跨字段方便 | combine 或拆文件 slices |
灵活性留给用户;不像 Redux 强制单一 reducer 树那么重。
5. 中间件为何是「洋葱模型」?
决策:中间件 = 高阶 StateCreator,由内向外包装 set/get。
| 收益 | 代价 |
|---|---|
| 核心零依赖;persist/devtools 可选 | 组合顺序需理解;类型折叠复杂 |
见 04-中间件系统。
6. 为什么用 Object.is 做快照比较?
决策:Vanilla 层 state 变更用 Object.is;React 层 snapshot 比较同样依赖 Object.is。
- 与 React 18 协调语义一致。
- 选择器应返回稳定引用(
useShallow)避免「值没变、引用变」导致重渲染。
7. 为什么 create 返回的是 Hook?
决策:const useStore = create(...) 符合 React 习惯,且隐藏 store api 传递。
| 收益 | 代价 |
|---|---|
| 符合 hooks 规则、Tree-shaking 友好 | 不能在不遵守 hooks 规则的地方调用 |
底层仍是 Vanilla StoreApi;useStore(api) 也可用于 Context 注入场景。
8. 为什么同时提供 Vanilla Store?
决策:createStore 与 create 并存。
场景:非 React 环境、测试、在订阅里驱动非 React UI、SSR 脚本、与 React 无关的 worker。
原则:一套状态、多种订阅面——React 只是订阅者之一。
总结
| 主题 | 一句话 |
|---|---|
| 无 Provider | 全局 store + 直接订阅,换简单与细粒度 |
| useSyncExternalStore | 并发与 SSR 正确性 |
| 浅合并 | 小而快,嵌套用 Immer/展开 |
| 中间件 | 可选增强,不膨胀核心 |
| Hook 形态 | 贴合 React,底层仍 Vanilla |
延伸阅读
- Zustand GitHub Discussions(v5 选择器 / 无限循环等)