跳到主要内容

use-sync-external-store

React.useSyncExternalStore 的向后兼容垫片。适用于于任何支持 Hooks 的 React

use-sync-external-storeReact 官方提供的一个 Hook , 主要解决 React 组件与外部储存(如 ReduxMobx 或自定义全局的状态 ) 的同步问题 。 诞生于 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+ 中是内置的
  • React 18 以下版本需要使用 use-sync-external-store/shim 垫片
信息

因为是 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. 核心参数: subscribegetSnapshotgetServerSnapshot

  • 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 ,监听 storedispatch 事件
  • getSnapshot 对应的 () => store.getState() ,返回当前 store 状态
  • getServerSnapshot 在 SSR 时返回服务端生成的初始 state