useSyncExternalStore 是一个 React Hook,允许你订阅一个外部 store。
const snapshot = useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot?)参考
useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot?)
在组件顶层调用 useSyncExternalStore,以便从外部数据 store 中读取一个值。
import { useSyncExternalStore } from 'react';
import { todosStore } from './todoStore.js';
function TodosApp() {
const todos = useSyncExternalStore(todosStore.subscribe, todosStore.getSnapshot);
// ...
}它返回 store 中数据的快照。你需要传入两个函数作为参数:
subscribe函数应该订阅该 store,并返回一个用于取消订阅的函数。getSnapshot函数应该从 store 中读取一份数据快照。
参数
-
subscribe:一个接收单个callback参数的函数,并将其订阅到 store。当 store 变化时,它应调用所提供的callback,这会让 React 重新调用getSnapshot,并在需要时重新渲染组件。subscribe函数应返回一个用于清理订阅的函数。 -
getSnapshot:一个返回组件所需 store 数据快照的函数。当 store 未变化时,对getSnapshot的重复调用必须返回相同的值。如果 store 发生变化且返回值不同(按Object.is比较),React 会重新渲染该组件。 -
可选
getServerSnapshot:一个返回 store 中数据初始快照的函数。它只会在服务端渲染期间,以及客户端对服务端渲染内容进行 hydration 期间使用。服务端快照必须在客户端和服务端之间保持一致,通常会被序列化并从服务端传递到客户端。如果省略这个参数,在服务端渲染组件时会抛出错误。
返回值
当前 store 的快照,你可以在渲染逻辑中使用它。
注意事项
-
getSnapshot返回的 store 快照必须是不可变的。如果底层 store 拥有可变数据,请在数据变化时返回一个新的不可变快照。否则,返回缓存的上一次快照。 -
如果在重新渲染期间传入了不同的
subscribe函数,React 会使用新传入的subscribe函数重新订阅该 store。你可以通过在组件外部声明subscribe来避免这一点。 -
如果在一次 非阻塞的 Transition 更新 期间 store 被修改,React 会回退为将该更新作为阻塞更新来执行。具体来说,对于每一次 Transition 更新,React 会在将变更应用到 DOM 之前再次调用一次
getSnapshot。如果它返回的值与最初调用时不同,React 会从头重新开始更新,这一次会作为阻塞更新执行,以确保屏幕上的每个组件都反映同一个 store 版本。 -
不建议基于
useSyncExternalStore返回的 store 值来在渲染中进行 suspend。原因是,外部 store 的变更无法被标记为 非阻塞的 Transition 更新,因此它们会触发最近的Suspensefallback,用加载指示器替换屏幕上已经渲染的内容,这通常会带来较差的用户体验。例如,以下做法不推荐:
const LazyProductDetailPage = lazy(() => import('./ProductDetailPage.js'));function ShoppingApp() {const selectedProductId = useSyncExternalStore(...);// ❌ 使用依赖于 `selectedProductId` 的 Promise 调用 `use`const data = use(fetchItem(selectedProductId))// ❌ 根据 `selectedProductId` 有条件地渲染懒加载组件return selectedProductId != null ? <LazyProductDetailPage /> : <FeaturedProducts />;}
用法
订阅外部 store
大多数 React 组件只会从它们的 props, state, 和 context. 中读取数据。然而,有时组件需要从 React 之外的某个会随时间变化的 store 中读取一些数据。这包括:
- 保存 React 之外状态的第三方状态管理库。
- 暴露可变值以及用于订阅其变化事件的浏览器 API。
在组件顶层调用 useSyncExternalStore,以便从外部数据 store 中读取一个值。
import { useSyncExternalStore } from 'react';
import { todosStore } from './todoStore.js';
function TodosApp() {
const todos = useSyncExternalStore(todosStore.subscribe, todosStore.getSnapshot);
// ...
}它返回 store 中数据的 快照。你需要传入两个函数作为参数:
subscribe函数 应该订阅该 store,并返回一个用于取消订阅的函数。getSnapshot函数 应该从 store 中读取一份数据快照。
React 会使用这些函数保持组件订阅该 store,并在发生变化时重新渲染。
例如,在下面的沙盒中,todosStore 被实现为一个位于 React 之外、用于存储数据的外部 store。TodosApp 组件通过 useSyncExternalStore Hook 连接到该外部 store。
import { useSyncExternalStore } from 'react'; import { todosStore } from './todoStore.js'; export default function TodosApp() { const todos = useSyncExternalStore(todosStore.subscribe, todosStore.getSnapshot); return ( <> <button onClick={() => todosStore.addTodo()}>添加待办</button> <hr /> <ul> {todos.map(todo => ( <li key={todo.id}>{todo.text}</li> ))} </ul> </> ); }
订阅浏览器 API
添加 useSyncExternalStore 的另一个原因是:当你想订阅浏览器暴露的某个会随时间变化的值时。例如,假设你希望组件显示网络连接是否处于活动状态。浏览器会通过名为 navigator.onLine. 的属性暴露这一信息。
这个值可以在 React 不知情的情况下变化,所以你应该使用 useSyncExternalStore 来读取它。
import { useSyncExternalStore } from 'react';
function ChatIndicator() {
const isOnline = useSyncExternalStore(subscribe, getSnapshot);
// ...
}要实现 getSnapshot 函数,请从浏览器 API 中读取当前值:
function getSnapshot() {
return navigator.onLine;
}接下来,你需要实现 subscribe 函数。例如,当 navigator.onLine 变化时,浏览器会在 window 对象上触发 online 和 offline 事件。你需要将 callback 参数订阅到相应事件上,然后返回一个用于清理这些订阅的函数:
function subscribe(callback) {
window.addEventListener('online', callback);
window.addEventListener('offline', callback);
return () => {
window.removeEventListener('online', callback);
window.removeEventListener('offline', callback);
};
}现在 React 知道如何从外部的 navigator.onLine API 中读取值,以及如何订阅它的变化。断开设备的网络连接,看看组件是否会响应式重新渲染:
import { useSyncExternalStore } from 'react'; export default function ChatIndicator() { const isOnline = useSyncExternalStore(subscribe, getSnapshot); return <h1>{isOnline ? '✅ 在线' : '❌ 已断开连接'}</h1>; } 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); }; }
将逻辑抽取到自定义 Hook 中
通常你不会直接在组件中编写 useSyncExternalStore。相反,你通常会从自己编写的自定义 Hook 中调用它。这让你可以在不同组件中使用同一个外部 store。
例如,这个自定义 useOnlineStatus Hook 会追踪网络是否在线:
import { useSyncExternalStore } from 'react';
export function useOnlineStatus() {
const isOnline = useSyncExternalStore(subscribe, getSnapshot);
return isOnline;
}
function getSnapshot() {
// ...
}
function subscribe(callback) {
// ...
}现在不同的组件可以调用 useOnlineStatus,而无需重复底层实现:
import { useOnlineStatus } from './useOnlineStatus.js'; function StatusBar() { const isOnline = useOnlineStatus(); return <h1>{isOnline ? '✅ 在线' : '❌ 已断开连接'}</h1>; } function SaveButton() { const isOnline = useOnlineStatus(); function handleSaveClick() { console.log('✅ 进度已保存'); } return ( <button disabled={!isOnline} onClick={handleSaveClick}> {isOnline ? '保存进度' : '正在重新连接...'} </button> ); } export default function App() { return ( <> <SaveButton /> <StatusBar /> </> ); }
添加对服务端渲染的支持
如果你的 React 应用使用了服务端渲染,你的 React 组件也会在浏览器环境之外运行,以生成初始 HTML。在连接外部 store 时,这会带来一些挑战:
- 如果你连接的是仅浏览器可用的 API,它将无法工作,因为它在服务器上不存在。
- 如果你连接的是第三方数据 store,你需要确保服务端和客户端上的数据一致。
要解决这些问题,请将一个 getServerSnapshot 函数作为 useSyncExternalStore 的第三个参数传入:
import { useSyncExternalStore } from 'react';
export function useOnlineStatus() {
const isOnline = useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
return isOnline;
}
function getSnapshot() {
return navigator.onLine;
}
function getServerSnapshot() {
return true; // 对于服务端生成的 HTML,始终显示“在线”
}
function subscribe(callback) {
// ...
}getServerSnapshot 函数类似于 getSnapshot,但它只在两种情况下运行:
- 它在服务器上生成 HTML 时运行。
- 它在客户端进行 hydration 时运行,也就是 React 接收服务端 HTML 并使其具备交互能力时运行。
这让你可以提供一个初始快照值,该值会在应用变得可交互之前使用。如果服务端渲染没有有意义的初始值,请省略此参数以强制在客户端渲染。
故障排除
我遇到一个错误:“getSnapshot 的结果应该被缓存”
这个错误意味着你的 getSnapshot 函数每次被调用时都会返回一个新对象,例如:
function getSnapshot() {
// 🔴 不要总是从 getSnapshot 返回不同的对象
return {
todos: myStore.todos
};
}如果 getSnapshot 的返回值与上一次不同,React 会重新渲染组件。这就是为什么如果你总是返回不同的值,就会进入无限循环并出现这个错误。
你的 getSnapshot 对象只有在某些内容确实发生变化时才应该返回不同的对象。如果你的 store 包含不可变数据,你可以直接返回这些数据:
function getSnapshot() {
// ✅ 你可以返回不可变数据
return myStore.todos;
}如果你的 store 数据是可变的,你的 getSnapshot 函数应该返回它的一个不可变快照。这意味着它 确实 需要创建新对象,但不应该在每次调用时都这样做。相反,它应该保存上一次计算出的快照,并在 store 中的数据没有改变时返回与上一次相同的快照。如何判断可变数据是否发生变化,取决于你的可变 store。
我的 subscribe 函数在每次重新渲染后都会被调用
这个 subscribe 函数定义在组件 内部,因此它在每次重新渲染时都不同:
function ChatIndicator() {
// 🚩 总是不同的函数,因此 React 会在每次重新渲染时重新订阅
function subscribe() {
// ...
}
const isOnline = useSyncExternalStore(subscribe, getSnapshot);
// ...
}如果你在重新渲染之间传入不同的 subscribe 函数,React 就会重新订阅你的 store。如果这导致性能问题,而你想避免重新订阅,可以把 subscribe 函数移到外部:
// ✅ 始终是同一个函数,因此 React 不需要重新订阅
function subscribe() {
// ...
}
function ChatIndicator() {
const isOnline = useSyncExternalStore(subscribe, getSnapshot);
// ...
}或者,把 subscribe 包装进 useCallback 中,这样只有在某个参数改变时才会重新订阅:
function ChatIndicator({ userId }) {
// ✅ 只要 userId 不变,就是同一个函数
const subscribe = useCallback(() => {
// ...
}, [userId]);
const isOnline = useSyncExternalStore(subscribe, getSnapshot);
// ...
}