Spec-Zone.ru › React 18

useSyncExternalStore

useSyncExternalStore — это React-хук, позволяющий подписываться на внешний хранилище.

const snapshot = useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot?)
  • Справочник
    • useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot?)
  • Использование
    • Подписка на внешнее хранилище
    • Подписка на API браузера
    • Извлечение логики в пользовательский хук
    • Добавление поддержки серверного рендеринга
  • Отладка
    • Ошибка: «Результат getSnapshot должен быть кэширован»
    • Моя функция subscribe вызывается после каждого перерендеринга

Справочник

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 вызовет getSnapshot во второй раз незадолго до применения изменений к DOM. Если он вернет другое значение, чем при первоначальном вызове, 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.

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 с useState и useReducer вместо него. API useSyncExternalStore в основном полезен, если вам необходимо интегрироваться с существующим кодом, не связанным с React.


Подписка на 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 изменяется, браузер вызывает события online и offline на объекте window . Вам нужно подписать аргумент callback на соответствующие события, а затем вернуть функцию очистки подписок:

function subscribe(callback) {
  window.addEventListener('online', callback);
  window.addEventListener('offline', callback);
  return () => {
    window.removeEventListener('online', callback);
    window.removeEventListener('offline', callback);
  };
}

Теперь React знает, как читать значение из внешнего API navigator.onLine и как подписываться на его изменения. Отключите устройство от сети и обратите внимание, что компонент перерисовывается в ответ:

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


Извлечение логики в пользовательский хук

Обычно вы не будете писать useSyncExternalStore напрямую в своих компонентах. Вместо этого вы обычно будете вызывать его из своего собственного пользовательского хука. Это позволяет использовать одно и то же внешнее хранилище из разных компонентов.

Например, этот пользовательский хук useOnlineStatus отслеживает, подключен ли интернет:

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
  };
}

React перерисует компонент, если значение, возвращаемое getSnapshot, отличается от последнего раза. Вот почему, если вы всегда возвращаете другое значение, вы попадете в бесконечный цикл и получите эту ошибку.

Ваш объект 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() {
    // ...
  }

  // ...
}

React подпишется на ваше хранилище повторно, если вы передадите другую функцию subscribe между перерисовками. Если это вызывает проблемы с производительностью, и вы хотите избежать повторной подписки, вынесите функцию 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]);

  // ...
}

© 2013–present Facebook Inc.
Licensed under the Creative Commons Attribution 4.0 International Public License.
https://18.react.dev/reference/react/useSyncExternalStore

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API