小而美的系列教程,周末定时更新,后续会推出基于 React 从 0~1 开发的三方插件,如果有收获,记得给个点赞。
相信很多同学都没使用过这个 API,官方描述为useSyncExternalStore is a React Hook that lets you subscribe to an external store,但如果你想开发一个 React 插件,那么它非常重要,由于这个 API 相对大多同学来说比较陌生,我们从以下两个方向阐述:
- 简介
- 使用案例
- 源码解读
- 总结
简介
React 官方文档中介绍了 useSyncExternalStore 的作用及用法:
useSyncExternalStore 是一个用于从外部数据源读取和订阅的 Hook,这个 Hooks 返回 store 的值并接受三个参数:
export function useSyncExternalStore<T>(
subscribe: (() => void) => () => void,
getSnapshot: () => T,
getServerSnapshot?: () => T,
): T
subscribe: 注册回调的函数,返回一个() => void 用于清除副作用函数,每当 store 更改时调用该回调函数触发组件更新
getSnapshot:返回对应(想要)的 store,在 store 不变的情况下,如果重复调用 getSnapshot 必须返回相同值,否则会触发更新,需要避免类似 { ...store } 这类问题
getServerSnapshot:返回服务器渲染期间使用的快照的函数,一般般用于 SSR 场景
使用案例
监听网络状态:如果我们想监听网络状态,并封装一个 hook,正常情况下官方案例:
function useOnlineStatus() {
// Not ideal: Manual store subscription in an Effect
const [isOnline, setIsOnline] = useState(true);
useEffect(() => {
function updateState() {
setIsOnline(navigator.onLine);
}
updateState();
window.addEventListener("online", updateState);
window.addEventListener("offline", updateState);
return () => {
window.removeEventListener("online", updateState);
window.removeEventListener("offline", updateState);
};
}, []);
return isOnline;
}
function ChatIndicator() {
const isOnline = useOnlineStatus();
// ...
}
官方使用 useSyncExternalStore 的案例:
export function useOnlineStatus() {
const isOnline = useSyncExternalStore(subscribe, getSnapshot);
return isOnline;
}
function getSnapshot() {
return navigator.onLine;
}
function subscribe(callback) {
window.addEventListener("online", callback);
window.addEventListener("offline", callback);
return () => {
window.removeEventListener("online", callback);
window.removeEventListener("offline", callback);
};
}
function ChatIndicator() {
const isOnline = useOnlineStatus();
// ...
}
是不是有种错觉“两种并无多大差别”?如果你只是简单看代码的话,确实没有多大差别。
最大的差别在于subscribe to an external store,前者必须依赖 setIsOnline 才能更新组件,也就是 useState;而后者不需要,我们可以做到数据与、逻辑的分离。
getSnapshot 仅负责返回我们需要使用到的 data,subscribe 用于监听 store 的变化(网络),同时调用 callback 触发组件更新。
相比前者,我们可以通过任意方式去改变 store,只要确保数据更新时能够触发 callback 的调用即可,非常利于三方库的开发,eg: React Query。
案例2:为了说明 external store,我们再看一个例子,用户在输入框输入内容时,我们将对应的数据赋值给外部的 store,在 subscribe 里面监听 blur 事件,当 blur 调用 callback,触发组件更新。
const store = {
words: "",
};
const getSnapshot = () => {
return store.words;
};
const App = () => {
const ref = useRef<HTMLInputElement>(null);
const subscribe = (callback: () => void) => {
ref.current?.addEventListener("blur", callback);
return () => {
ref.current?.removeEventListener("blur", callback);
};
};
const words = useSyncExternalStore(subscribe, getSnapshot);
return (
<>
<p>words: {words}</p>
<input
type="text"
ref={ref}
onChange={(e) => {
store.words = e.target.value;
}}
/>
</>
);
};
export default App;
源码解读
前者通过 useState 触发组件更新,后者又是如何触发组件更新的呢?
其实还是 useState ,只不过做了一层封装,源码地址:github.com/facebook/re…
简化后的代码如下:
export function useSyncExternalStore<T>(
subscribe: (() => void) => () => void,
getSnapshot: () => T,
getServerSnapshot?: () => T,
) {
const value = getSnapshot();
const [{inst}, forceUpdate] = useState({inst: {value, getSnapshot}});
useLayoutEffect(() => {
inst.value = value;
inst.getSnapshot = getSnapshot;
if (checkIfSnapshotChanged(inst)) {
// Force a re-render.
forceUpdate({inst});
}
}, [subscribe, value, getSnapshot]);
useEffect(() => {
// Check for changes right before subscribing. Subsequent changes will be
// detected in the subscription handler.
if (checkIfSnapshotChanged(inst)) {
// Force a re-render.
forceUpdate({inst});
}
const handleStoreChange = () => {
if (checkIfSnapshotChanged(inst)) {
// Force a re-render.
forceUpdate({inst});
}
};
// Subscribe to the store and return a clean-up function.
return subscribe(handleStoreChange);
}, [subscribe]);
return value;
}
function checkIfSnapshotChanged<T>(inst: {
value: T,
getSnapshot: () => T,
}): boolean {
const latestGetSnapshot = inst.getSnapshot;
const prevValue = inst.value;
try {
const nextValue = latestGetSnapshot();
return !is(prevValue, nextValue);
} catch (error) {
return true;
}
}
源码实现非常简单,首先是调用 getSnapshot 初始化 value,并将 value 和 getSnapshot 通过 state 维护起来
其次是监听 subscribe, value, getSnapshot 的变化,如果有变化,直接更新
最后是监听 subscribe ,返回 subscribe 的调用,并将 handleStoreChange 作为形参传入,对应 callback,当监听到 online 状态时,触发 handleStoreChange 的调用,从而触发 forceUpdate
function subscribe(callback) {
window.addEventListener("online", callback);
return () => {
window.removeEventListener("online", callback);
};
}
总结
useSyncExternalStore 相比其他 API,理解成本会高一点,但它的设计思想非常好,值得研究。