useSyncExternalStore

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:

  1. subscribe 函数应该订阅存储并返回一个取消订阅的函数。

    ¥The subscribe function should subscribe to the store and return a function that unsubscribes.

  2. getSnapshot 函数应该从存储中读取数据的快照。

    ¥The getSnapshot function should read a snapshot of the data from the store.

请参阅下面的更多示例。

¥See more examples below.

参数

¥Parameters

  • subscribe:一个接受单个 callback 参数并将其订阅到存储的函数。当存储发生变化时,它应该调用提供的 callback。这将导致组件重新渲染。subscribe 函数应该返回一个清理订阅的函数。

    ¥subscribe: A function that takes a single callback argument and subscribes it to the store. When the store changes, it should invoke the provided callback. This will cause the component to re-render. The subscribe 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 to getSnapshot must return the same value. If the store changes and the returned value is different (as compared by Object.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 passed subscribe function. You can prevent this by declaring subscribe 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 nearest Suspense 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:

  1. subscribe 函数 应该订阅存储并返回取消订阅的函数。

    ¥The subscribe function should subscribe to the store and return a function that unsubscribes.

  2. 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>
    </>
  );
}

注意

如果可能,我们建议使用 useStateuseReducer 的内置 React 状态。如果你需要与现有的非 React 代码集成,useSyncExternalStore API 最有用。

¥When possible, we recommend using built-in React state with useState and useReducer instead. The useSyncExternalStore API is mostly useful if you need to integrate with existing non-React code.


订阅浏览器 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 对象上触发 onlineoffline 事件。你需要将 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.

注意

确保 getServerSnapshot 在初始客户端渲染上返回与在服务器上返回的数据完全相同的数据。例如,如果 getServerSnapshot 在服务器上返回了一些预填充的存储内容,则需要将这些内容传输到客户端。一种方法是在服务器渲染期间发出一个 <script> 标记,该标记设置一个类似于 window.MY_STORE_DATA 的全局变量,并在 getServerSnapshot 中从客户端读取该全局变量。你的外部存储应提供有关如何执行此操作的说明。

¥Make sure that getServerSnapshot returns the same exact data on the initial client render as it returned on the server. For example, if getServerSnapshot returned some prepopulated store content on the server, you need to transfer this content to the client. One way to do this is to emit a <script> tag during server rendering that sets a global like window.MY_STORE_DATA, and read from that global on the client in getServerSnapshot. Your external store should provide instructions on how to do that.


故障排除

¥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]);

// ...
}

React 中文网 - 粤ICP备13048890号