认识
一、认识
useSyncExternalStore
是 React 18
引入的一个内置 Hook
, 专门用于解决外部数据源(比如 Redux``、Zustand
等状态管理库)与 React
组件之间的数据同步问题。它的主要作用是让组件在渲染期间能够安全、一致地读取外部 store
的状态快照, 同时在 store
发生变化时,自动触发组件重新渲染,从而确保 UI
始终与外部状态保持同步。具体来说,它需要你提供两个核心函数:
-
subscribe
: 一个函数, 接收一个单独的callback
参数并把它订阅到store
上。当store
发生改变时会调用提供的callback
,这将导致React
重新调用getSnapshot
并在需要的时候重新渲染组件。subscribe
函数会返回清除订阅的函数。换句话说:subscribe
的作用就是让每个使用store
的组件都能注册自己的订阅回调。当store
更新时, 我们手动调用订阅回调, 从而让相应的组件通过getSnapshot
获取最新状态并触发重新渲染。所以subscribe
注册的回调数目实际上就反映了当前有多少组件(或其他订阅者)在关注这个store
的变化,同时它也支持取消订阅以避免不必要的更新。 -
getSnapshot
: 一个函数,返回组件需要的store
中的数据快照。在store
不变的情况下,重复调用getSnapshot
必须返回同一个值。如果store
改变,并且返回值也不同了(用Object.is
比较),React
就会重新渲染组件。 -
返回值: 该
store
的当前快照,可以在你的渲染逻辑中使用。
如果我们更新了 store
, 循环遍历 subscribe
注册的回调, 会通知 React
重新调用 getSnapshot
并在需要的时候重新渲染组件。这样,即使在并发渲染模式下,也能防止出现 撕裂(tearing
)现象,保证组件在整个渲染周期内读取到的数据是一致且稳定的。总之,useSyncExternalStore
为外部状态和 React
渲染之间提供了一种标准化、健壮且高效的连接方式,是实现精准订阅和高性能更新的关键工具。
二、语法
const state = { count: 1 };
const listener = new Set();
const subscribe = (callback)=>{
listener.add(callback);
return () => listeners.delete(listener);
}
const getSnapshot = ()=> state;
const setState = ()=>{
const previousState = state;
state = { ...state, count: 2};
listener.forEach(callback => callback(state, previousState));
}
const snapshot = useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot?);
-
subscribe
: 一个函数,接收一个单独的callback
参数并把它订阅到store
上。当store
发生改变,它应当调用被提供的callback
。这会导致组件重新渲染。subscribe
函数会返回清除订阅的函数。 -
getSnapshot
: 一个函数,返回组件需要的store
中的数据快照。在store
不变的情况下,重复调用getSnapshot
必须返回同一个值。如果store
改变,并且返回值也不同了(用Object.is
比较),React
就会重新渲染组件。 -
getServerSnapshot
: 一个函数,返回store
中数据的初始快照。它只会在服务端渲染时,以及在客户端进行服务端渲染内容的hydration
时被用到。快照在服务端与客户端之间必须相同,它通常是从服务端序列化并传到客户端的。如果你忽略此参数,在服务端渲染这个组件会抛出一个错误。 -
snapshot
: 该store
的当前快照,可以在你的渲染逻辑中使用。
三、Polyfill
在 React 17
中没有 useSyncExternalStore
,我们通常会通过自定义 Hook
或使用官方提供的 polyfill
来实现类似的功能。基本思路是用 useState
来保存外部 store
的快照,然后利用 useEffect
订阅 store
的更新。在订阅回调中调用 getSnapshot
获取最新状态,再用 setState
更新组件,从而触发重新渲染。
import { useEffect, useRef, useState } from "react";
function useSyncExternalStore(subscribe, getSnapshot) {
const [snapshot, setSnapshot] = useState(() => getSnapshot());
const getSnapshotRef = useRef(getSnapshot);
getSnapshotRef.current = getSnapshot;
useEffect(() => {
function handleStoreChange() {
const newSnapshot = getSnapshotRef.current();
setSnapshot((prevSnapshot) =>
Object.is(prevSnapshot, newSnapshot) ? prevSnapshot : newSnapshot
);
}
const unsubscribe = subscribe(handleStoreChange);
handleStoreChange();
return unsubscribe;
}, [subscribe]);
return snapshot;
}