React 18 引入了 useSyncExternalStore,这是一个允许组件从 React 树之外的任何数据源(如浏览器 API、WebSockets 或第三方 store)读取数据的 Hook,且不会出现以往通过 useState 和 useEffect 镜像数据时常见的视觉闪烁问题。如果你曾因为 UI 的一部分显示旧的窗口大小,而另一部分已经反映了新大小时看到过界面闪烁,那么这个 Hook 正是你一直在寻找的解决方案。
为什么外部状态需要一个新的 Hook
React 组件一直能够订阅超出其自身作用域的事物:window 对象、Redux store 或 WebSocket 连接。传统的方法是在 useEffect 中设置订阅,通过 setState 将传入的值推送到本地状态,并让 React 重新渲染。当 React 进行同步渲染时,这种方法运行良好;但 React 18 引入了并发渲染(concurrent rendering),在这种模式下,组件在浏览器实际绘制之前可能会被渲染多次。在这种模式下,“镜像到本地状态”的模式可能会导致 tearing(撕裂)——即在单个渲染周期内,UI 的不同部分读取了同一个外部值的不同快照。
useSyncExternalStore 的设计初衷就是为了防止 tearing。它向外部数据源请求一个 snapshot(当前值)和一个 subscribe 函数(如何接收变更通知)。React 会在组件挂载时调用 subscribe,并在卸载时自动取消订阅。每当 getSnapshot 返回的 snapshot 发生变化时,React 会调度一次渲染,确保整个组件树读取的是 同一个 snapshot,从而保证一致性。
两个必需的函数
| 函数 | 功能描述 |
|---|---|
| subscribe | 接收一个回调函数,每当外部值发生变化时必须调用该回调。它会返回一个 unsubscribe 函数,React 会在清理时调用它。 |
| getSnapshot | 从外部数据源返回当前值。它必须是 引用稳定的(referentially stable)——即在每次渲染时都返回相同的函数对象——以便 React 能够正确比较 snapshot。 |
浏览器 online 状态的一个最小化实现如下:
function useOnlineStatus() {
return useSyncExternalStore(
(onChange) => {
window.addEventListener('online', onChange);
window.addEventListener('offline', onChange);
return () => {
window.removeEventListener('online', onChange);
window.removeEventListener('offline', onChange);
};
},
() => navigator.onLine
);
}
当任何组件调用 useOnlineStatus() 时,只有在 navigator.onLine 真正发生变化时,React 才会重新渲染该组件,并且每次渲染看到的都是相同的值。
防止 Hook 出错的规则
仅返回组件所需的最少量数据。
如果外部 store 持有一个大型对象,但组件只关心其中一个字段,请仅返回该字段。返回过大的数据会导致不必要的重新渲染。对
getSnapshot进行记忆化(Memoise)。
如果你在每次渲染时都重新创建该函数,React 会将其视为一个新数据源,从而可能导致无限循环。请使用useCallback对其进行包装,或者将其定义在组件外部。避免在每次调用时返回新的对象。
返回一个新对象(例如{ count: store.getCount() })会在每次渲染时创建一个新的引用,使 React 认为 snapshot 已更改,从而触发无限渲染循环。请使用原始类型(primitives)、字符串、数字或经过记忆化的对象。不要将其用于组件内部状态。
对于 仅 存在于组件内部的数据,useState仍然是正确的工具。useSyncExternalStore会带来本地状态不需要的额外开销。
何时使用它
- 浏览器 API – 窗口大小、媒体查询、
navigator.onLine、电池状态。 - 框架无关的 store – Zustand、Redux、MobX 或任何暴露了订阅 API 的自定义 store。
- 在 React 之外更新的可变数据源 – WebSocket 消息、IndexedDB 变更事件、Service Worker 通知。
如果你的数据源已经存在于 React 内部(例如父组件的状态),请继续使用 useState 或 context。
社区评价
早期使用者反馈,在并发渲染设置下调整窗口大小时,useSyncExternalStore 消除了他们之前看到的闪烁现象。一些库已经将其内部 Hook 切换到了此 API,承诺在不同 React 版本中提供更可预测的行为。代价是心智负担略有增加:开发者必须考虑这两个函数的稳定性,并避免在每次调用时返回新对象。
下一步关注
React 未来的版本可能会收紧围绕 useSyncExternalStore 的规范,并可能为常见的浏览器 API 添加内置辅助函数。请密切关注 React 官方博客,留意旧的模仿 useEffect 模式是否会被弃用。与此同时,该 Hook 已趋于稳定并属于公共 API 的一部分,因此你可以放心地重构现有的外部状态代码。
快速体验
一个展示了窗口大小同步、在线/离线检测以及简单 Zustand store 的在线演示位于 https://usesyncexternalstore.vercel.app/。包含完整注释的源代码可在 https://github.com/dev48v/usesyncexternalstore 获取。
核心要点: useSyncExternalStore 为 React 提供了一个可靠的桥梁,用于连接任何存在于其渲染周期之外的可变数据,从而防止撕裂 (tearing) 和不必要的重新渲染。将其应用于外部数据源,保持快照 (snapshot) 尽可能精简且稳定,并将一致性维护的繁重工作交给 React 来处理。
