use-sync-external-store
React.useSyncExternalStore 的向后兼容垫片。适用于于任何支持 Hooks 的 React。
use-sync-external-store 是 React 官方提供的一个 Hook , 主要解决 React 组件与外部储存(如 Redux 、 Mobx 或自定义全局的状态 ) 的同步问题 。 诞生于 React 16.8 版本,旨在优化外部状态与 React 渲染的集成,尤其是在并发模式( Concurrent Mode )下的表现,并支持服务端渲染 ( 「SSR」 )
一、 核心作用
该 Hook 的核心目标是让 React 组件能更高效、可靠地订阅外部状态储存的变化,并在状态更新时触发重新渲染。
- 外部状态与 React 的绑定 : 当使用外部状态管理库( 如 Redux ) 时, React 组件需要感知外部储存的变化并重新渲染。 use-sync-external-store 提供了一种标准化的订阅机制,避免了手动订阅/取消订阅逻辑
- 避免不必要的渲染 : 通过
getSnapshot函数返回外部状态的快照, React 会对前后进行对比,尽在快照变化时进行触发重渲染,减少冗余计算 - 服务端 ( SSR ) 支持 : 提供了
getServerSnapshot接口,用于在服务端生成初始状态的快照,确保服务端渲染的内容与客户端初始化状态一只,避免水合 (Hydration) 错误 - 并发模式兼容 : 在并发渲染时,确保外部状态的更新能被正确的最终,避免渲染过程中的竞态条件 ( Race Condition )
1. 状态同步时机
React 18 引入并发渲染后,组件渲染可能被中断、暂停或恢复。如果组件直接订阅外部状态(如通过 useEffect 手动订阅),可能会:
- 渲染过程中外部状态突变,导致渲染结果基于“过时状态”
- 并发更新中多次渲染时,状态快照不一致,引发 UI 闪烁或逻辑错误
所以:
- 在组件渲染时确保状态一致(使用
useLayoutEffect) - 避免在渲染过程中状态不一致导致的“状态撕裂”
2. 兼容性考虑
信息
因为是 React 18+ 内置的,所以源码核心部分代码就是:
'use strict';
import * as React from 'react';
export const useSyncExternalStore = React.useSyncExternalStore;
if (__DEV__) {
// 在测试环境的提示
console['error'](
`这个包是 react 18+ 用的,
如果是想用 React 16 或是 17 ,
请从 "user-sync-external-store/shim" 中导入。
当本地不可用时,它会回退到模拟实现`,
);
}
二、核心关注的点
该包的核心是导出了 useSyncExternalStore 钩子:
function useSyncExternalStore<Snapshot>(
subscribe: (callback: () => void) => () => void, // 订阅外部状态变化的函数
getSnapshot: () => Snapshot, // 获取当前状态快照的函数
getServerSnapshot?: () => Snapshot, // 服务端渲染时的初始快照
): Snapshot;
1. 核心参数: subscribe 、 getSnapshot 、 getServerSnapshot
subscribe(必选) : 一个函数,接收一个callback作为参数,用于注册外部状态变化的监听器。当外部状态变化时,subscribe内部触发callback(通常是通知 React 需要更新 )subscribe使用时必须在卸载时通过返回的清理函数取消订阅( React 会自动处理 ),避免内存泄漏
getSnapshot(必选) :一个函数,返回当前外部状态的“快照”(通常是状态副本或不可变值)。 React 会通过对比前后两次渲染的快照,判断是否需要重新渲染组件- 快照需保证稳定性(相同的状态返回相同的引用),否则可能导致不必要的渲染
getServerSnapshot(可选, SSR 专用) : 尽在服务端渲染时调用,返回服务端生成的外部状态快照。若未提供, React 会回退到getSnapshot(将可能导致水合错误)
2. 订阅与取消订阅的生命周期
- 组件挂载时, React 会调用
subscribe注册监听器,监听器会在状态变化时触发 React 的更新流程 - 组件卸载时, React 会自动调用
subscribe返回的清理函数(若有),取消订阅 subscribe应确保同一组件实例的监听器只注册一次,避免重复订阅
3. 快照对比与渲染优化
React 会缓存上一次渲染的 getSnapshot 结果,并在下次渲染时对比新旧快照 :
- 若快照相同(通过
Object.is比较),跳过重新渲染 - 若不同,触发组件更新,并重新获取快照
- 避免僵尸树 : 如果父组件因状态变化重新渲染时,而子组件依赖的外部状态未更新,
use-sync-external-store会确保子组件使用最新的快照,避免子组件停留在旧状态
4. 并发模式下的行为
在并发渲染中, use-sync-external-store 会确保:
- 外部状态的更新不会打断正在进行的渲染,而是通过标记“脏组件”来调度更新
- 快照的获取四幂等的,避免并发渲染时的竞态条件(如多次调用
getSnapshot导致不一致)
5. SSR 水合一致性
若使用服务端渲染,必须提供 getServerSnapshot ,且返回值需与客户端初始 getSnapshot 一致。否则水合时因快照不匹配报错。
三、 流程分析
use-sync-external-store 的工作流程可分为 挂载 、 更新 、 卸载 三个阶段。
1. 组件挂载阶段
- React 调用
useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot) - 若存在
getServerSnapshot( SSR 场景 ),使用其返回值作为初始快照;否则调用getSnapshot获取初始快照 - 调用
subscribe注册监听器,监听器会触发 React 的更新回调(例如标记组件为“需要更新”)
2. 组件渲染阶段
- React 调用
getSnapshot获取当前外部状态的快照 - 对比当前快照与上一次渲染的快照(通过
Object.is):- 若相同,跳过重新渲染,直接使用旧结果
- 若不通,更新缓存的快照,并触发组件重新渲染
3. 组件更新阶段
- 重新渲染时,再次调用
getSnapshot获取最新的快照,并重复上述对比逻辑
4. 组件卸载阶段
- React 自动调用
subscribe返回的清理函数(通常是取消订阅的逻辑),移除对外部状态的监听
四、 实际应用场景
以 Redux 为例, React-Redux v8+ 内部使用 use-sync-external-store 实现组件与 Redux store 绑定:
subscribe对应store.subscribe,监听store的dispatch事件getSnapshot对应的() => store.getState(),返回当前store状态getServerSnapshot在 SSR 时返回服务端生成的初始state