并发渲染和僵尸子组件问题
React 18 并发渲染可打断、恢复 render。外部 store 若订阅方式不当,会出现 tearing(撕裂) 和 zombie child(僵尸子组件)。Zustand v4+ 依赖 useSyncExternalStore 规避这两类问题。
1. 并发下两类经典问题
1.1 Tearing(撕裂)
同一次「逻辑渲染」中,组件 A 与 B 通过各自闭包读到 不同版本的 store,UI 互不一致。
典型诱因:useState + useEffect(subscribe) 在 render 与 effect 之间 store 已变,但子组件仍用旧 snapshot。
1.2 Zombie Child(僵尸子组件)
父更新导致子组件已从树中移除,但此前排队的订阅回调仍执行,回调里使用卸载时的 props/闭包 state,与当前树不一致。
列表项删除、路由切换、条件渲染关闭分支时最常见。
2. useSyncExternalStore 如何解决
| 能力 | 作用 |
|---|---|
渲染期 getSnapshot | 同一 render _pass 内快照一致 |
Object.is 比较 | 快照未变不触发重渲染 |
| 与 React 调度同步的 subscribe | 卸载后不再对已卸载组件触发 setState |
getServerSnapshot | SSR 首帧一致 |
Zustand 的 useStore 把 store 的 subscribe / getState / getInitialState 接到上述 API,无需业务代码处理并发补丁。
3. 使用侧仍要注意
| 场景 | 建议 |
|---|---|
列表子项订阅 items.find(id) | 父删项后子卸载;避免在已卸载组件的 effect 里依赖过期 item |
| 选择器返回新对象 | 用 useShallow,避免额外重渲染与边界竞态 |
在 render 里 getState() 写 store | 反模式;写在事件/ effect |
| SSR | 提供与水合一致的初始 state;persist 注意 hydration mismatch |
4. 与 React Redux 文档的关系
「Stale props and zombie children」最初在 react-redux 语境下讨论。Zustand 无 connect 层,但外部 store + 错误订阅的并发问题同构;解法统一到 useSyncExternalStore。
5. 和 React 文档的衔接
- React 并发:06-Suspense 与并发
useSyncExternalStore:04-Hooks §9.4- 集成实现:03-React 集成
6. 总结
并发安全不是 Zustand 在 reconciler 里做了特殊 case,而是用对接 React 官方外部 store 协议保证快照一致、订阅生命周期正确。选型上应用 Zustand 4+ 且 React 18+;勿自写 useState 订阅 store。