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。这将导致组件重新渲染。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 组件仅从其 propsstatecontext 中读取数据。但是,有时组件需要从 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>
    </>
  );
}

注意

如果可能,我们建议使用内置的 React 状态,使用 useStateuseReducer。只有当您需要与现有的非 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]);

// ...
}