useSyncExternalStore 是一个 React Hook,它允许你订阅外部存储。
const snapshot = useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot?)参考
🌐 Reference
useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot?)
在组件的顶层调用 useSyncExternalStore 以从外部数据存储中读取值。
🌐 Call useSyncExternalStore at the top level of your component to read a value from an external data store.
import { useSyncExternalStore } from 'react';
import { todosStore } from './todoStore.js';
function TodosApp() {
const todos = useSyncExternalStore(todosStore.subscribe, todosStore.getSnapshot);
// ...
}它返回存储中数据的快照。你需要传递两个函数作为参数:
🌐 It returns the snapshot of the data in the store. You need to pass two functions as arguments:
subscribe函数应该订阅存储,并返回一个取消订阅的函数。getSnapshot函数应该从存储中读取数据快照。
参数
🌐 Parameters
subscribe:一个接收单个callback参数并将其订阅到 store 的函数。当 store 发生变化时,它应该调用提供的callback,这将导致 React 重新调用getSnapshot并(如果需要)重新渲染组件。subscribe函数应该返回一个清理订阅的函数。getSnapshot:一个返回组件所需的存储中数据快照的函数。当存储未更改时,对getSnapshot的重复调用必须返回相同的值。如果存储发生更改且返回的值不同(通过Object.is比较),React 将重新渲染组件。- 可选
getServerSnapshot:一个返回存储中数据初始快照的函数。它只会在服务器端渲染期间以及在客户端对服务器渲染内容进行水化时使用。服务器快照在客户端和服务器之间必须保持一致,通常会被序列化并从服务器传递到客户端。如果省略此参数,在服务器上渲染组件将会抛出错误。
返回
🌐 Returns
你可以在渲染逻辑中使用的存储的当前快照。
🌐 The current snapshot of the store which you can use in your rendering logic.
注意事项
🌐 Caveats
-
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 />;}
用法
🌐 Usage
订阅外部存储
🌐 Subscribing to an external store
你大多数的 React 组件只会从它们的 props、state 和 context 中读取数据。然而,有时组件需要从 React 外部的一些随时间变化的存储中读取数据。这包括:
🌐 Most of your React components will only read data from their props, state, and context. However, sometimes a component needs to read some data from some store outside of React that changes over time. This includes:
- 在 React 之外保存状态的第三方状态管理库。
- 公开可变值和事件以订阅其更改的浏览器 API。
在组件的顶层调用 useSyncExternalStore 以从外部数据存储中读取值。
🌐 Call useSyncExternalStore at the top level of your component to read a value from an external data store.
import { useSyncExternalStore } from 'react';
import { todosStore } from './todoStore.js';
function TodosApp() {
const todos = useSyncExternalStore(todosStore.subscribe, todosStore.getSnapshot);
// ...
}它返回存储中数据的 快照 。你需要传入两个函数作为参数:
- The
subscribe函数 应该订阅 store 并返回一个取消订阅的函数。 - The
getSnapshot函数 应当从存储中读取数据的快照。
React 将使用这些函数来保持你的组件订阅存储并在更改时重新渲染它。
🌐 React will use these functions to keep your component subscribed to the store and re-render it on changes.
例如,在下面的沙箱中,todosStore 实现为一个在 React 外部存储数据的外部存储。TodosApp 组件使用 useSyncExternalStore Hook 连接到该外部存储。
🌐 For example, in the sandbox below, todosStore is implemented as an external store that stores data outside of React. The TodosApp component connects to that external store with the 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
🌐 Subscribing to a browser API
添加 useSyncExternalStore 的另一个原因是在你想订阅浏览器公开的随时间变化的某些值时。例如,假设你希望你的组件显示网络连接是否活跃。浏览器通过一个名为 navigator.onLine 的属性公开此信息。
🌐 Another reason to add useSyncExternalStore is when you want to subscribe to some value exposed by the browser that changes over time. For example, suppose that you want your component to display whether the network connection is active. The browser exposes this information via a property called navigator.onLine.
此值可能在 React 不知道的情况下更改,因此你应该使用 useSyncExternalStore 来读取它。
🌐 This value can change without React’s knowledge, so you should read it with useSyncExternalStore.
import { useSyncExternalStore } from 'react';
function ChatIndicator() {
const isOnline = useSyncExternalStore(subscribe, getSnapshot);
// ...
}要实现 getSnapshot 函数,请从浏览器 API 读取当前值:
🌐 To implement the getSnapshot function, read the current value from the browser API:
function getSnapshot() {
return navigator.onLine;
}接下来,你需要实现 subscribe 函数。例如,当 navigator.onLine 发生变化时,浏览器会在 window 对象上触发 online 和 offline 事件。你需要将 callback 参数订阅到相应的事件,然后返回一个用于清理订阅的函数:
🌐 Next, you need to implement the subscribe function. For example, when navigator.onLine changes, the browser fires the online and offline events on the window object. You need to subscribe the callback argument to the corresponding events, and then return a function that cleans up the subscriptions:
function subscribe(callback) {
window.addEventListener('online', callback);
window.addEventListener('offline', callback);
return () => {
window.removeEventListener('online', callback);
window.removeEventListener('offline', callback);
};
}现在 React 知道如何从外部 navigator.onLine API 读取数值以及如何订阅其变化。将你的设备从网络断开,并注意组件会随之重新渲染:
🌐 Now React knows how to read the value from the external navigator.onLine API and how to subscribe to its changes. Disconnect your device from the network and notice that the component re-renders in response:
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); }; }
将逻辑提取到自定义钩子
🌐 Extracting the logic to a custom Hook
通常你不会直接在你的组件中写 useSyncExternalStore。相反,你通常会从你自己的自定义 Hook 调用它。这让你可以在不同的组件中使用相同的外部存储。
🌐 Usually you won’t write useSyncExternalStore directly in your components. Instead, you’ll typically call it from your own custom Hook. This lets you use the same external store from different components.
例如,这个自定义 useOnlineStatus Hook 用于跟踪网络是否在线:
🌐 For example, this custom useOnlineStatus Hook tracks whether the network is online:
import { useSyncExternalStore } from 'react';
export function useOnlineStatus() {
const isOnline = useSyncExternalStore(subscribe, getSnapshot);
return isOnline;
}
function getSnapshot() {
// ...
}
function subscribe(callback) {
// ...
}现在不同的组件可以调用 useOnlineStatus,而无需重复底层实现:
🌐 Now different components can call useOnlineStatus without repeating the underlying implementation:
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 /> </> ); }
添加对服务器渲染的支持
🌐 Adding support for server rendering
如果你的 React 应用使用服务器渲染, 你的 React 组件也将在浏览器环境之外运行,以生成初始的 HTML。这在连接外部存储时会带来一些挑战:
🌐 If your React app uses server rendering, your React components will also run outside the browser environment to generate the initial HTML. This creates a few challenges when connecting to an external store:
- 如果你连接到仅限浏览器的 API,它将无法工作,因为它在服务器上不存在。
- 如果你要连接到第三方数据存储,则需要其数据在服务器和客户端之间匹配。
为了解决这些问题,将一个 getServerSnapshot 函数作为第三个参数传递给 useSyncExternalStore:
🌐 To solve these issues, pass a getServerSnapshot function as the third argument to 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,但它仅在两种情况下运行:
🌐 The getServerSnapshot function is similar to getSnapshot, but it runs only in two situations:
- 它在生成 HTML 时在服务器上运行。
- 它在客户端运行,在hydration期间,即当 React 获取服务器 HTML 并使其可交互时。
这让你提供初始快照值,该值将在应用变得可交互之前使用。如果服务器渲染没有有意义的初始值,请省略此参数以强制在客户端渲染
🌐 This lets you provide the initial snapshot value which will be used before the app becomes interactive. If there is no meaningful initial value for the server rendering, omit this argument to force rendering on the client.
故障排除
🌐 Troubleshooting
我收到一个错误:“getSnapshot 的结果应该被缓存”
🌐 I’m getting an error: “The result of getSnapshot should be cached”
此错误意味着你的 getSnapshot 函数每次调用时都会返回一个新对象,例如:
🌐 This error means your getSnapshot function returns a new object every time it’s called, for example:
function getSnapshot() {
// 🔴 Do not return always different objects from getSnapshot
return {
todos: myStore.todos
};
}如果 getSnapshot 的返回值与上次不同,React 将重新渲染组件。这就是为什么如果你总是返回不同的值,你会进入无限循环并收到此错误的原因。
🌐 React will re-render the component if getSnapshot return value is different from the last time. This is why, if you always return a different value, you will enter an infinite loop and get this error.
你的 getSnapshot 对象只有在实际发生变化时才应该返回不同的对象。如果你的存储包含不可变数据,你可以直接返回该数据:
🌐 Your getSnapshot object should only return a different object if something has actually changed. If your store contains immutable data, you can return that data directly:
function getSnapshot() {
// ✅ You can return immutable data
return myStore.todos;
}如果你的存储数据是可变的,你的 getSnapshot 函数应该返回该数据的不可变快照。这意味着它确实需要创建新对象,但不应在每次调用时都这样做。相反,它应该存储上一次计算的快照,如果存储中的数据没有变化,则返回上一次相同的快照。你如何判断可变数据是否发生变化取决于你的可变存储。
🌐 If your store data is mutable, your getSnapshot function should return an immutable snapshot of it. This means it does need to create new objects, but it shouldn’t do this for every single call. Instead, it should store the last calculated snapshot, and return the same snapshot as the last time if the data in the store has not changed. How you determine whether mutable data has changed depends on your mutable store.
我的 subscribe 函数在每次重新渲染后都会被调用
🌐 My subscribe function gets called after every re-render
这个 subscribe 函数是在组件 内部 定义的,因此每次重新渲染时它都是不同的:
🌐 This subscribe function is defined inside a component so it is different on every re-render:
function ChatIndicator() {
// 🚩 Always a different function, so React will resubscribe on every re-render
function subscribe() {
// ...
}
const isOnline = useSyncExternalStore(subscribe, getSnapshot);
// ...
}如果你在重新渲染时传递了不同的 subscribe 函数,React 将会重新订阅你的 store。如果这会导致性能问题,并且你想避免重新订阅,请将 subscribe 函数移到外部:
🌐 React will resubscribe to your store if you pass a different subscribe function between re-renders. If this causes performance issues and you’d like to avoid resubscribing, move the subscribe function outside:
// ✅ Always the same function, so React won't need to resubscribe
function subscribe() {
// ...
}
function ChatIndicator() {
const isOnline = useSyncExternalStore(subscribe, getSnapshot);
// ...
}或者,将 subscribe 封装到 useCallback 中,以便只有当某些参数发生变化时才重新订阅:
🌐 Alternatively, wrap subscribe into useCallback to only resubscribe when some argument changes:
function ChatIndicator({ userId }) {
// ✅ Same function as long as userId doesn't change
const subscribe = useCallback(() => {
// ...
}, [userId]);
const isOnline = useSyncExternalStore(subscribe, getSnapshot);
// ...
}