useSyncExternalStore
是一个 React 钩子,可让你订阅外部存储。
¥useSyncExternalStore
is a React Hook that lets you subscribe to an external store.
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
函数应该订阅存储并返回一个取消订阅的函数。¥The
subscribe
function should subscribe to the store and return a function that unsubscribes. -
getSnapshot
函数应该从存储中读取数据的快照。¥The
getSnapshot
function should read a snapshot of the data from the store.
参数
¥Parameters
-
subscribe
:一个接受单个callback
参数并将其订阅到存储的函数。当 store 发生变化时,它应该调用提供的callback
,这将导致 React 重新调用getSnapshot
并(如果需要)重新渲染组件。subscribe
函数应该返回一个清理订阅的函数。¥
subscribe
: A function that takes a singlecallback
argument and subscribes it to the store. When the store changes, it should invoke the providedcallback
, which will cause React to re-callgetSnapshot
and (if needed) re-render the component. Thesubscribe
function should return a function that cleans up the subscription. -
getSnapshot
:返回组件所需的存储中数据快照的函数。虽然存储没有改变,但重复调用getSnapshot
必须返回相同的值。如果 store 发生变化并且返回值不同(使用Object.is
相比),React 会重新渲染该组件。¥
getSnapshot
: A function that returns a snapshot of the data in the store that’s needed by the component. While the store has not changed, repeated calls togetSnapshot
must return the same value. If the store changes and the returned value is different (as compared byObject.is
), React re-renders the component. -
可选
getServerSnapshot
:返回存储中数据的初始快照的函数。它将仅在服务器渲染期间和客户端上服务器渲染内容的混合期间使用。服务器快照在客户端和服务器之间必须是相同的,并且通常被序列化并从服务器传递到客户端。如果省略此参数,则在服务器上渲染组件将引发错误。¥optional
getServerSnapshot
: A function that returns the initial snapshot of the data in the store. It will be used only during server rendering and during hydration of server-rendered content on the client. The server snapshot must be the same between the client and the server, and is usually serialized and passed from the server to the client. If you omit this argument, rendering the component on the server will throw an error.
返回
¥Returns
你可以在渲染逻辑中使用的存储的当前快照。
¥The current snapshot of the store which you can use in your rendering logic.
注意事项
¥Caveats
-
getSnapshot
返回的存储快照必须是不可变的。如果底层存储有可变数据,如果数据已更改,则返回一个新的不可变快照。否则,返回缓存的最后一个快照。¥The store snapshot returned by
getSnapshot
must be immutable. If the underlying store has mutable data, return a new immutable snapshot if the data has changed. Otherwise, return a cached last snapshot. -
如果在重新渲染期间传递了不同的
subscribe
函数,React 将使用新传递的subscribe
函数重新订阅存储。你可以通过在组件外部声明subscribe
来防止这种情况。¥If a different
subscribe
function is passed during a re-render, React will re-subscribe to the store using the newly passedsubscribe
function. You can prevent this by declaringsubscribe
outside the component. -
如果存储在 非阻塞转换更新 期间发生变化,React 将回退到以阻塞方式执行该更新。具体来说,对于每个 Transition 更新,React 将在将更改应用于 DOM 之前第二次调用
getSnapshot
。如果它返回的值与最初调用时不同,React 将从头开始重新启动更新,这次将其作为阻塞更新应用,以确保屏幕上的每个组件都反映相同版本的存储。¥If the store is mutated during a non-blocking Transition update, React will fall back to performing that update as blocking. Specifically, for every Transition update, React will call
getSnapshot
a second time just before applying changes to the DOM. If it returns a different value than when it was called originally, React will restart the update from scratch, this time applying it as a blocking update, to ensure that every component on screen is reflecting the same version of the store. -
不建议根据
useSyncExternalStore
返回的存储值暂停渲染。原因是外部存储的突变无法标记为 非阻塞转换更新,因此它们将触发最近的Suspense
回退,用加载旋转器替换屏幕上已渲染的内容,这通常会导致糟糕的用户体验。¥It’s not recommended to suspend a render based on a store value returned by
useSyncExternalStore
. The reason is that mutations to the external store cannot be marked as non-blocking Transition updates, so they will trigger the nearestSuspense
fallback, replacing already-rendered content on screen with a loading spinner, which typically makes a poor UX.例如,不鼓励以下行为:
¥For example, the following are discouraged:
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 组件只会从它们的 属性, 状态, 和 上下文 中读取数据。但是,有时组件需要从 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 之外保存状态的第三方状态管理库。
¥Third-party state management libraries that hold state outside of React.
-
公开可变值和事件以订阅其更改的浏览器 API。
¥Browser APIs that expose a mutable value and events to subscribe to its changes.
在组件的顶层调用 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
函数 应该订阅存储并返回取消订阅的函数。¥The
subscribe
function should subscribe to the store and return a function that unsubscribes. -
getSnapshot
函数 应该从存储中读取数据的快照。¥The
getSnapshot
function should read a snapshot of the data from the store.
React 将使用这些函数来保持你的组件订阅存储并在更改时重新渲染它。
¥React will use these functions to keep your component subscribed to the store and re-render it on changes.
例如,在下面的沙箱中,todosStore
被实现为一个外部存储,在 React 之外存储数据。TodosApp
组件使用 useSyncExternalStore
钩子连接到该外部存储。
¥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
。而是,你通常会从自己的自定义钩子中调用它。这使你可以使用来自不同组件的相同外部存储。
¥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
钩子跟踪网络是否在线:
¥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,它将无法工作,因为它在服务器上不存在。
¥If you’re connecting to a browser-only API, it won’t work because it does not exist on the server.
-
如果你要连接到第三方数据存储,则需要其数据在服务器和客户端之间匹配。
¥If you’re connecting to a third-party data store, you’ll need its data to match between the server and client.
要解决这些问题,请将 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 时在服务器上运行。
¥It runs on the server when generating the HTML.
-
它在 hydration 期间在客户端上运行,即当 React 获取服务器 HTML 并使其交互时。
¥It runs on the client during hydration, i.e. when React takes the server HTML and makes it interactive.
这使你可以提供初始快照值,该值将在应用变为交互式之前使用。如果服务器渲染没有有意义的初始值,则省略此参数给 强制在客户端渲染。
¥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() {
const isOnline = useSyncExternalStore(subscribe, getSnapshot);
// 🚩 Always a different function, so React will resubscribe on every re-render
function subscribe() {
// ...
}
// ...
}
如果你在重新渲染之间传递不同的 subscribe
函数,React 将重新订阅你的存储。如果这会导致性能问题并且你希望避免重新订阅,请将 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:
function ChatIndicator() {
const isOnline = useSyncExternalStore(subscribe, getSnapshot);
// ...
}
// ✅ Always the same function, so React won't need to resubscribe
function subscribe() {
// ...
}
或者,将 subscribe
封装到 useCallback
中以仅在某些参数更改时重新订阅:
¥Alternatively, wrap subscribe
into useCallback
to only resubscribe when some argument changes:
function ChatIndicator({ userId }) {
const isOnline = useSyncExternalStore(subscribe, getSnapshot);
// ✅ Same function as long as userId doesn't change
const subscribe = useCallback(() => {
// ...
}, [userId]);
// ...
}