useSyncExternalStore

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

它返回数据源中数据的快照。你需要传入两个函数作为参数

  1. subscribe 函数应该订阅数据源并返回一个取消订阅的函数。
  2. 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);
// ...
}

它返回存储中数据的快照。需要传入两个函数作为参数:

  1. subscribe函数应订阅存储并返回一个取消订阅的函数。
  2. 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>
    </>
  );
}

注意

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


订阅浏览器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 对象上触发 onlineoffline 事件。您需要将 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 并使其具有交互性)期间运行。

这允许您提供初始快照值,该值将在应用程序变得可交互之前使用。如果服务器端渲染没有有意义的初始值,请省略此参数以 强制在客户端渲染。

注意

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


故障排除

我收到一个错误:“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]);

// ...
}