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
。这将导致组件重新渲染。subscribe
函数应该返回一个清理订阅的函数。 -
getSnapshot
:一个函数,它返回组件所需的数据快照。虽然存储没有改变,但重复调用getSnapshot
必须返回相同的值。如果存储发生更改并且返回值不同(通过Object.is
比较),React 将重新渲染组件。 -
可选
getServerSnapshot
:一个函数,它返回存储中数据的初始快照。它只在服务器渲染和客户端对服务器渲染内容进行水合时使用。服务器快照在客户端和服务器之间必须相同,并且通常被序列化并从服务器传递到客户端。如果你省略此参数,则在服务器上渲染组件将引发错误。
返回值
当前存储的快照,你可以在渲染逻辑中使用它。
注意事项
-
getSnapshot
返回的存储快照必须是不可变的。如果底层存储具有可变数据,则在数据更改时返回一个新的不可变快照。否则,返回缓存的最后一个快照。 -
如果在重新渲染期间传递了不同的
subscribe
函数,React 将使用新传递的subscribe
函数重新订阅存储。您可以通过在组件外部声明subscribe
来防止这种情况。 -
如果在 非阻塞过渡更新期间更改了存储,React 将回退到执行该更新作为阻塞更新。具体来说,对于每个过渡更新,React 会在将更改应用于 DOM 之前再次调用
getSnapshot
。如果它返回的值与最初调用时不同,React 将从头开始重新启动更新,这次将其作为阻塞更新应用,以确保屏幕上的每个组件都反映存储的相同版本。 -
不建议根据
useSyncExternalStore
返回的存储值来*暂停*渲染。原因是无法将对外部存储的更改标记为 非阻塞过渡更新,因此它们将触发最近的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、state 和 context 中读取数据。但是,有时组件需要从 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]);
// ...
}