useSyncExternalStore
是一个 React Hook,允许你订阅外部数据源。
const snapshot = useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot?)
参考
useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot?)
在组件的顶层调用 useSyncExternalStore
来读取外部数据源的值。
import { useSyncExternalStore } from 'react';
import { todosStore } from './todoStore.js';
function TodosApp() {
const todos = useSyncExternalStore(todosStore.subscribe, todosStore.getSnapshot);
// ...
}
它返回数据源中数据的快照。你需要传入两个函数作为参数
subscribe
函数应该订阅数据源并返回一个取消订阅的函数。getSnapshot
函数应该从数据源读取数据的快照。
参数
-
subscribe
: 一个接收单个callback
参数并将其订阅到数据源的函数。当数据源更改时,它应该调用提供的callback
,这将导致 React 重新调用getSnapshot
并(如果需要)重新渲染组件。subscribe
函数应该返回一个清理订阅的函数。 -
getSnapshot
: 一个从数据源返回组件所需数据快照的函数。当数据源未更改时,对getSnapshot
的重复调用必须返回相同的值。如果数据源更改并且返回值不同(通过Object.is
比较),React 将重新渲染组件。 -
可选
getServerSnapshot
: 一个返回数据源初始数据快照的函数。它仅在服务端渲染和客户端水化服务端渲染内容期间使用。客户端和服务端之间的服务端快照必须相同,通常会被序列化并从服务端传递到客户端。如果你省略此参数,在服务端渲染组件将抛出错误。
返回值
当前存储的快照,可在渲染逻辑中使用。
注意事项
-
由
getSnapshot
返回的存储快照必须是不可变的。如果底层存储包含可变数据,则在数据更改时返回新的不可变快照。否则,返回缓存的最后一个快照。 -
如果在重新渲染期间传递了不同的
subscribe
函数,React将使用新传递的subscribe
函数重新订阅存储。可以通过在组件外部声明subscribe
来防止这种情况。 -
如果在非阻塞式Transition更新期间修改了存储,React将回退到将该更新作为阻塞式更新执行。具体来说,对于每个Transition更新,React将在应用更改到DOM之前第二次调用
getSnapshot
。如果它返回的值与最初调用时不同,React将从头开始重新启动更新,这次将其作为阻塞式更新应用,以确保屏幕上的每个组件都反映存储的相同版本。 -
不建议根据
useSyncExternalStore
返回的存储值来*挂起*渲染。原因是外部存储的变异不能标记为非阻塞式Transition更新,因此它们将触发最近的Suspense
回退,用加载旋转器替换屏幕上已渲染的内容,这通常会带来较差的用户体验。例如,以下是不建议的做法
const LazyProductDetailPage = lazy(() => import('./ProductDetailPage.js'));function ShoppingApp() {const selectedProductId = useSyncExternalStore(...);// ❌ Calling `use` with a Promise dependent on `selectedProductId`const data = use(fetchItem(selectedProductId))// ❌ Conditionally rendering a lazy component based on `selectedProductId`return selectedProductId != null ? <LazyProductDetailPage /> : <FeaturedProducts />;}
用法
订阅外部存储
大多数React组件只读取其props、状态和上下文中的数据。但是,有时组件需要读取来自React外部随时间变化的某个存储中的某些数据。这包括:
- 保存React外部状态的第三方状态管理库。
- 公开可变值和事件以订阅其更改的浏览器API。
在组件的顶层调用 useSyncExternalStore
来读取外部数据源的值。
import { useSyncExternalStore } from 'react';
import { todosStore } from './todoStore.js';
function TodosApp() {
const todos = useSyncExternalStore(todosStore.subscribe, todosStore.getSnapshot);
// ...
}
它返回存储中数据的快照。需要传入两个函数作为参数:
subscribe
函数应订阅存储并返回一个取消订阅的函数。getSnapshot
函数应从存储中读取数据的快照。
React将使用这些函数使您的组件保持订阅到存储,并在更改时重新渲染。
例如,在下面的沙箱中,todosStore
实现为一个外部存储,用于存储React外部的数据。TodosApp
组件使用useSyncExternalStore
Hook连接到该外部存储。
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()}>Add todo</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 ? '✅ Online' : '❌ Disconnected'}</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 中调用它。这允许您从不同的组件使用相同的外部存储。
例如,这个自定义的 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 ? '✅ Online' : '❌ Disconnected'}</h1>; } function SaveButton() { const isOnline = useOnlineStatus(); function handleSaveClick() { console.log('✅ Progress saved'); } return ( <button disabled={!isOnline} onClick={handleSaveClick}> {isOnline ? 'Save progress' : 'Reconnecting...'} </button> ); } export default function App() { return ( <> <SaveButton /> <StatusBar /> </> ); }
添加对服务器端渲染的支持
如果您的 React 应用使用 服务器端渲染,您的 React 组件也会在浏览器环境之外运行以生成初始 HTML。这在连接到外部存储时会带来一些挑战。
- 如果您连接到仅限浏览器的 API,它将无法工作,因为它在服务器端不存在。
- 如果您连接到第三方数据存储,则需要其数据在服务器端和客户端之间匹配。
为了解决这些问题,将 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; // Always show "Online" for server-generated HTML
}
function subscribe(callback) {
// ...
}
getServerSnapshot
函数类似于 getSnapshot
,但它只会在两种情况下运行。
- 它在服务器端生成 HTML 时运行。
- 它在客户端 水合(即 React 获取服务器 HTML 并使其具有交互性)期间运行。
这允许您提供初始快照值,该值将在应用程序变得可交互之前使用。如果服务器端渲染没有有意义的初始值,请省略此参数以 强制在客户端渲染。
故障排除
我收到一个错误:“getSnapshot
的结果应被缓存”
此错误意味着您的 getSnapshot
函数每次调用时都会返回一个新对象,例如:
function getSnapshot() {
// 🔴 Do not return always different objects from getSnapshot
return {
todos: myStore.todos
};
}
如果 getSnapshot
的返回值与上次不同,React 将重新渲染组件。因此,如果您始终返回不同的值,则会进入无限循环并收到此错误。
您的 getSnapshot
对象只有在实际发生更改时才应返回不同的对象。如果您的存储包含不可变数据,您可以直接返回该数据。
function getSnapshot() {
// ✅ You can return immutable data
return myStore.todos;
}
如果您的存储数据是可变的,您的 getSnapshot
函数应返回其不可变快照。这意味着它确实需要创建新对象,但它不应该每次调用都这样做。相反,它应该存储上次计算的快照,如果存储中的数据没有更改,则返回与上次相同的快照。如何确定可变数据是否已更改取决于您的可变存储。
我的 subscribe
函数在每次重新渲染后都会被调用
这个 subscribe
函数定义在组件 *内部*,因此在每次重新渲染时都不同。
function ChatIndicator() {
const isOnline = useSyncExternalStore(subscribe, getSnapshot);
// 🚩 Always a different function, so React will resubscribe on every re-render
function subscribe() {
// ...
}
// ...
}
如果您在重新渲染之间传递不同的 subscribe
函数,React 将重新订阅您的存储。如果这会导致性能问题,并且您想避免重新订阅,请将 subscribe
函数移到外部。
function ChatIndicator() {
const isOnline = useSyncExternalStore(subscribe, getSnapshot);
// ...
}
// ✅ Always the same function, so React won't need to resubscribe
function subscribe() {
// ...
}
或者,将 subscribe
包装到 useCallback
中,以便仅在某些参数更改时重新订阅。
function ChatIndicator({ userId }) {
const isOnline = useSyncExternalStore(subscribe, getSnapshot);
// ✅ Same function as long as userId doesn't change
const subscribe = useCallback(() => {
// ...
}, [userId]);
// ...
}