React useSyncExternalStore订阅外部数据源保持一致性
React 18 引入了并发特性,允许 React 中断、恢复或放弃渲染。如果在组件渲染过程中读取了外部数据源(如全局状态、浏览器 API 等),并在渲染间隙发生了数据变更,可能会导致 UI 显示不一致(即“撕裂”现象)。useSyncExternalStore 旨在解决这一问题,确保外部数据源与 React 渲染状态保持同步。
1. 理解核心参数与机制
在使用该 Hook 前,理解其三个核心参数的职责至关重要。该 Hook 强制外部数据源的读取与订阅遵循 React 的渲染周期。
查看函数签名如下:
const state = useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot?)
各参数的具体作用如下:
| 参数名 | 类型 | 作用描述 |
|---|---|---|
subscribe |
函数 | 订阅外部存储的回调函数。接收一个 callback 参数,当存储变更时,调用该 callback。返回一个取消订阅的清理函数。 |
getSnapshot |
函数 | 返回外部数据源的当前快照。这个快照必须是不可变的(Immutable)。如果快照没有变化,React 将不会重新渲染组件。 |
getServerSnapshot |
函数 | 可选。用于服务端渲染(SSR)时提供初始快照。如果应用不支持 SSR,可以省略该参数。 |
2. 实战:订阅浏览器网络状态
以监听浏览器在线/离线状态为例,演示如何将 navigator.onLine 这个外部数据源与 React 组件同步。
步骤 1:定义获取快照函数
编写 getSnapshot 函数,用于直接读取当前的网络状态。
const getSnapshot = () => {
return navigator.onLine;
};
步骤 2:定义订阅函数
编写 subscribe 函数。这需要向浏览器 API 注册事件监听,并告诉 React 在事件触发时更新组件。
const subscribe = (callback) => {
// 当网络状态变化时,调用 callback 通知 React
window.addEventListener('online', callback);
window.addEventListener('offline', callback);
// 返回清理函数,用于组件卸载时移除监听
return () => {
window.removeEventListener('online', callback);
window.removeEventListener('offline', callback);
};
};
步骤 3:在组件中使用 Hook
调用 useSyncExternalStore 将上述逻辑整合。
import { useSyncExternalStore } from 'react';
function NetworkStatus() {
// 传入订阅和快照函数
const isOnline = useSyncExternalStore(subscribe, getSnapshot);
return (
<div>
<h1>当前状态: {isOnline ? '在线' : '离线'}</h1>
</div>
);
}
通过这种方式,无论是 React 的并发渲染,还是外部浏览器 API 的触发,都能保证界面显示的 isOnline 状态与 navigator.onLine 严格一致。
3. 处理服务端渲染(SSR)兼容性
如果你的应用需要支持服务端渲染(如 Next.js),必须处理服务端环境没有 window 对象或 navigator 对象的问题。
步骤 1:添加服务端快照
定义 getServerSnapshot 函数,返回一个默认的初始值(通常默认为 true 或 false,取决于业务逻辑)。
const getServerSnapshot = () => {
return true; // 服务器默认假设用户在线
};
步骤 2:更新 Hook 调用
修改组件中的 Hook 调用,传入第三个参数。
function NetworkStatus() {
const isOnline = useSyncExternalStore(
subscribe,
getSnapshot,
getServerSnapshot // 传入服务端快照函数
);
return <h1>{isOnline ? '在线' : '离线'}</h1>;
}
这样,React 在服务端渲染时会使用 getServerSnapshot,而在客户端 hydration 后会自动切换使用 getSnapshot。
4. 数据更新流程可视化
理解数据如何在 React 和外部存储之间流转,有助于避免 Bug。下图展示了当外部数据发生变化时,React 如何感知并触发更新。
注意流程中的关键点:React 并不直接“监听”数据,而是依赖你在 subscribe 中显式 调用 传给你的 callback 函数。如果你忘记在数据变化时调用它,界面将不会更新。
5. 进阶用法:封装通用外部 Store Hook
为了复用逻辑,可以封装一个通用的 Hook 来管理任何具有 subscribe 和 getState 模式的存储(如 Redux、Zustand 或自定义 Store)。
假设有一个简单的 Store 对象结构如下:
// 外部 Store 定义
let store = {
state: { count: 0 },
listeners: new Set(),
getState() {
return this.state;
},
subscribe(listener) {
this.listeners.add(listener);
return () => this.listeners.delete(listener);
},
setState(newState) {
this.state = newState;
this.listeners.forEach(listener => listener());
}
};
步骤 1:创建自定义 Hook
编写 useExternalStore 封装函数。
import { useSyncExternalStore } from 'react';
export function useExternalStore(externalStore, selector) {
// getSnapshot:获取数据并应用选择器
const getSnapshot = () => {
const state = externalStore.getState();
return selector ? selector(state) : state;
};
// subscribe:直接使用外部 store 的订阅方法
const subscribe = (callback) => {
return externalStore.subscribe(callback);
};
return useSyncExternalStore(subscribe, getSnapshot);
}
步骤 2:在业务组件中使用
使用封装好的 Hook 替代直接调用。
function Counter() {
// 仅订阅 count 字段,而非整个 state
const count = useExternalStore(store, (state) => state.count);
return (
<div>
<p>计数: {count}</p>
<button onClick={() => store.setState({ count: count + 1 })}>
**增加**
</button>
</div>
);
}
通过这种方式,你既利用了 useSyncExternalStore 的并发安全特性,又保持了代码的整洁和可复用性。确保 selector 函数是纯函数,且返回的值是稳定的引用或基本类型,以避免不必要的重渲染。

暂无评论,快来抢沙发吧!