skip to content
usubeni fantasy logo Usubeni Fantasy

看完就懂 useSyncExternalStore

/ 7 分钟阅读

功能

React 引入 useSyncExternalStore 也很长一段时间了,但是存在感还不太强。简而言之,它专门用来搞定那些不受 React 内部生命周期控制的外部数据源

过去最大的问题其实是 React 渲染时的 「撕裂」,这是 React 为了优化页面响应速度引入的并发渲染机制带来的副作用。

简单来说就是 React 为了防止在渲染时长时间无法响应用户输入,把渲染过程拆分成多个可中断的小任务,这就能小任务的间隙中插入用户响应,从而模拟出「并发」的感觉。更完整的前因后果可以参考《React 的设计哲学》

在 React 并发渲染机制下,如果用普通的 useEffect 去同步外部数据,可能会出现渲染进行到一半时数据突然发生变化,导致同一份页面中,一半的组件拿着老数据,另一半拿着新数据的灵异现象(但是实际上出现这个问题的几率其实非常小,大家都忽略了,这就导致了 useSyncExternalStore 的存在感很低)。使用 useSyncExternalStore 后,如果在渲染过程中快照发生变化,React 会丢弃当前渲染并重新开始,从而保证同一次提交中的所有组件看到的是同一个版本的数据。

使用场景

订阅浏览器 API

拿监听网络状态来说。不使用这个 Hook 之前,我们通常得在组件里写个包含完整挂载和清理逻辑的 useEffect 去监听 onlineoffline 事件。

function subscribe(callback) {
window.addEventListener("online", callback);
window.addEventListener("offline", callback);
return () => {
window.removeEventListener("online", callback);
window.removeEventListener("offline", callback);
};
}
function getSnapshot() {
return navigator.onLine;
}
// 组件里直接这么用
const isOnline = useSyncExternalStore(subscribe, getSnapshot);

监听媒体查询(Media Queries)响应式布局也是同样的套路:

const query = window.matchMedia("(max-width: 600px)");
function subscribe(callback) {
query.addEventListener("change", callback);
return () => query.removeEventListener("change", callback);
}
const isMobile = useSyncExternalStore(subscribe, () => query.matches);

轻量级全局状态

如果你接手了一个极小的项目,不想引入 Redux 或 Zustand 这样繁琐的包,但又迫切需要在几个跨层级的组件间共享某部分状态。这时候你可以直接手搓一个简易的 Store:

// 丢在 React 外面的状态中心
let internalState = { count: 0 };
const listeners = new Set();
const store = {
increment() {
internalState = { count: internalState.count + 1 };
listeners.forEach((l) => l());
},
subscribe(callback) {
listeners.add(callback);
return () => listeners.delete(callback);
},
getSnapshot() {
return internalState;
},
};
// 任何组件里都可以直接同步获取状态
const state = useSyncExternalStore(store.subscribe, store.getSnapshot);

注意:useSyncExternalStore 内部用 Object.is 比较前后快照,如果 getSnapshot 在数据未变的情况下每次都返回新对象,会导致无限循环重渲染。

只要把这段代码看懂,你就掌握了 Zustand 这种现代状态管理库的核心原理

竞品 API

useEffect + setState

曾经大家都习惯在 useEffect 里监听外部变化,如果变了,再跑一下 setState 触发更新。

这就又到了日常批判 useEffect 的时候了。

useEffect 带来重复渲染和闪烁问题。如果你的外部状态和页面初始计算的状态不对齐,页面渲染就会经历「旧值 -> 闪烁 -> 新值」这三步。而 useSyncExternalStore 在渲染中途就能直接取走最新的正确值。

另外,在处理服务端渲染时,用副作用很容易抛出水合(Hydration)错误,因为服务端和客户端首次生成的 HTML 大概率因为外部数据对不上。useSyncExternalStore 为此专门开了一个叫 getServerSnapshot 的参数,让你传能兜底服务端的静态快照。

Context

很多人滥用 Context 做全局状态,但如果是频繁变动的数据,Context 的广播机制简直是一场灾难。只要 Provider 提供的值发生了变动,它底下所有的子组件也会跟着无脑重跑 Render,除非你给每个组件层级套一层 React.memo(当然现在有 compiler,但也不是毫无代价)。

相比之下,useSyncExternalStore 实现了高精度的按需订阅——只有从 Store 取出的快照真的有了变化,关联的组件才会再次渲染。在这里还是顺便强调一下,没事别用 Context。

总结

要判断何时使用 useSyncExternalStore 其实很简单,只要你的数据依然在 React 的生命周期里流转(例如表单实时输入、控制弹窗开闭的布尔值),那就老老实实用回你的 useStateuseReducer

一旦数据满足游离于 React 管理之外、会随时间变化、且你要让 UI 能自动响应这种变化这三个条件,就毫不犹豫上 useSyncExternalStore。日常写前端页面也许碰不到几次,但之后你要是去造底层 Hook 库,或者需要硬啃第三方库内部暴露出的状态时,useSyncExternalStore 绝对好使~

相关链接

推荐阅读

评论组件加载中……