zustand 解决了三个难题
你引用的这段话出自 Zustand 作者 Paul Henschel 之口,正好点出了 Zustand 相比其他状态管理库(尤其是 Redux 和 MobX)的几个“隐藏优势”——解决了 React 生态中长期存在的三个棘手问题:
- Zombie Child Problem(僵尸子组件问题)
- React Concurrency(React 并发特性,如
startTransition、useDeferredValue) - Context Loss Between Mixed Renderers(混合渲染器间的上下文丢失)
下面逐一拆解每个问题是什么,以及 Zustand 是如何解决的。
一、Zombie Child Problem(僵尸子组件问题)
问题场景
在 React 中,状态更新与组件渲染是异步批处理的。当一个状态更新导致父组件重新渲染,而父组件内部通过某些方式(如 Redux 的 connect、直接读 store)获取了新状态,然后传递 props 给子组件。如果子组件在同一次更新中也尝试从 store 读取同一个状态,可能出现“时间不一致”——子组件读到的是旧状态,而父组件传递的 props 是基于新状态的。这就会产生“僵尸子组件”:子组件展示的数据与父组件预期的不一致,且在后续渲染中永远不会自动修复。
经典例子(使用 Redux + connect 或旧版 useSelector):
// 父组件
function Parent() {
const items = useSelector(state => state.items); // 假设 items 从 [] 变为 [1,2,3]
return <Child items={items} />
}
// 子组件
function Child({ items }) {
const selectedId = useSelector(state => state.selectedId); // 可能读到旧值,比如 null,而 items 已经是新数组
// 导致 selectedId 对应的 item 在 items 中找不到
}
为什么叫“zombie”?因为这个子组件看起来活着(渲染了),但它引用的数据是“死去”的、不一致的。在 React 16 及以前的并发模式未正式启用时,这个问题相对可控;但 React 18 引入并发特性后,渲染可以被中断和恢复,问题变得更加严重。
Zustand 的解决方案
Zustand 通过 useSyncExternalStore(React 18 官方 hook)彻底解决了这个问题。useSyncExternalStore 的设计目标之一就是保证在一次同步的 store 快照下读取多个值的一致性。具体机制:
useSyncExternalStore要求传入一个getSnapshot函数,该函数必须返回一个不可变的状态快照。- React 在每一次渲染中,都会原子地获取
getSnapshot()的返回值,并在整个组件树中保持一致。 - 当 store 更新时,React 会同步地重新执行
getSnapshot,如果返回值变化,则触发重新渲染。 - 关键点:组件在单次渲染中多次调用
getSnapshot都会拿到相同的快照(因为快照是在渲染开始时固定的),从而避免了父组件读到一个版本、子组件读到另一个版本的问题。
Zustand 的 useStore hook 正是基于 useSyncExternalStore 实现的,所以天然免疫 zombie child。
补充:在 React 18 之前,Zustand 自己实现了一套基于
useReducer+useLayoutEffect的兼容方案,同样避免了这个坑。
二、React Concurrency(React 并发特性)
问题背景
React 18 引入了 并发渲染(Concurrent Rendering),其中两个重要 API:
startTransition:标记非紧急更新,允许渲染被中断。useDeferredValue:延迟某个值的更新。
在并发模式下,组件的渲染可能被多次“启动-中断-恢复”。这对于传统状态管理库(如 Redux)是巨大的挑战,因为 Redux 的 dispatch 会立即触发订阅者计算,而 React 可能还未完成上一次渲染。典型问题包括:
- tearing(撕裂):同一个状态在同一个渲染批次中,不同组件读到不同的版本(因为渲染被中断后恢复时,store 可能已经变了)。
- 高频率更新导致性能下降:每个
dispatch都会触发useSelector的重新计算,即使在被中断的渲染中也会执行。
Zustand 的解决方案
Zustand 通过 useSyncExternalStore + 可变状态快照 + 精细化更新调度 来应对并发:
-
useSyncExternalStore内置并发安全:这个 hook 是 React 官方为 Concurrent Rendering 设计的,它知道如何挂起、恢复和丢弃过时的渲染。当组件渲染被中断时,getSnapshot返回的状态不会“污染”最终输出;当渲染恢复时,它会重新检查最新的 snapshot,如果变化则取消本次渲染并重新开始。 -
选择性更新与原子快照:Zustand 的 store 是 不可变更新(默认使用
Object.assign浅合并,或配合 Immer),但getSnapshot返回的是当前整个 state 对象。React 可以通过比较 snapshot 是否变化来决定是否重渲染,这种比较是严格且并发的。 -
避免不必要的计算:Zustand 的 selector 机制本身就具有细粒度缓存。当并发中断发生时,selector 不会重复执行(除非必要的 snapshot 变化)。Redux 的
useSelector每次 store 变化都会运行 selector,在并发模式下可能导致浪费。 -
没有“外部调度器”:Redux 有自己的中间件和异步逻辑(如 thunk、saga),这些逻辑与 React 的并发调度器无法直接通信。Zustand 完全依赖 React 自身的调度,状态更新就是
setState(同步或异步),没有额外的调度层,因此天然与 React 并发模型对齐。
三、Context Loss Between Mixed Renderers(混合渲染器间的上下文丢失)
问题场景
一个 React 应用可能同时包含 不同渲染器:
- React DOM(用于 Web)
- React Native(用于移动端)
- React Three Fiber(基于 React 的 Three.js 渲染器)
- React Canvas、React PDF 等
这些渲染器各自维护独立的 React Context 实例。当你想在一个渲染器生成的组件中使用另一个渲染器的 Context(比如在 React Three Fiber 的 canvas 中读取 React DOM 的 Context),通常无法直接访问——这就是“上下文丢失”。
具体表现:你用 useContext(MyContext) 在 Three Fiber 的组件中读取数据,得到的总是 undefined,因为 Three Fiber 的 reconciler 运行在独立的 Fiber 树中,不共享外层的 Context.
Zustand 的解决方案
Zustand 的 store 根本不依赖 React Context。它的核心是框架无关的闭包 store,任何组件(无论运行在哪个渲染器下)只要调用 useStore(或者直接 getState())就能访问同一个内存中的 store 对象。
因此:
- 在 React DOM 组件中修改
useBoundStore.setState(...) - 在 React Three Fiber 组件中通过
useBoundStore()读取 - 甚至在非 React 环境(如 Web Worker、Node.js)中也可以读取
不存在“上下文丢失”问题,因为 Zustand 压根没用 Context。
相比之下,Redux 的 Provider 依赖 React Context,如果你在 Three Fiber 组件中 useSelector,需要确保该组件被同一个 Provider 包裹——但两个渲染器的组件树是分离的,无法穿透。Zustand 绕过了整个 Context 机制。
总结:为什么 Zustand 能同时解决这三个问题?
| 问题 | 原因 | Zustand 的解法 |
|---|---|---|
| Zombie Child | 异步更新导致父子组件读到不同版本的 store 数据 | 基于 useSyncExternalStore 保证单次渲染中的快照一致性 |
| React Concurrency | 并发中断和恢复可能造成 tearing 或重复计算 | useSyncExternalStore 原生支持并发安全 + 原子快照 + 精细 selector 缓存 |
| Context Loss | 不同渲染器有独立的 React Context 实例 | Zustand store 不依赖 Context,是所有渲染器共享的普通 JavaScript 对象 |
这三条正是 Zustand 作者引以为傲的“爪子”——看起来小巧可爱(1.2kB),但解决了其他库(包括 Redux Toolkit、MobX)都没能完美兼顾的底层难题。这也是为什么 Zustand 在复杂应用(尤其是 React Three Fiber 生态、跨渲染器场景)中越来越受欢迎的原因。