useSyncExternalStore

useSyncExternalStore це хук React, який дозволяє підписатися на зовнішній магазин.

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: Функція, яка приймає єдиний аргумент зворотного виклику і підписує його на магазин. Коли магазин змінюється, він повинен викликати наданий зворотний виклик. Це призведе до повторного рендерингу компонента. Функція subscribe повинна повертати функцію, яка очищає підписку.

  • getSnapshot: Функція, яка повертає знімок даних у сховищі, необхідний компоненту. Поки сховище не змінилося, повторні виклики getSnapshot повинні повертати те саме значення. Якщо сховище змінюється і повернуте значення відрізняється (порівняно з Object.is), React повторно відрендерить компонент.

  • опція getServerSnapshot: Функція, яка повертає початковий знімок даних у сховищі. Він буде використовуватися лише під час рендерингу на сервері та під час гідратації відрендереного на сервері контенту на клієнтській стороні. Знімок сервера повинен бути однаковим на клієнтській стороні та сервері, і зазвичай він серіалізується і передається від сервера до клієнта. Якщо ви пропустите цей аргумент, рендеринг компонента на сервері призведе до помилки.

Повернення

Поточний знімок сховища, який ви можете використовувати у своїй логіці рендерингу.

Застереження

  • Знімок сховища, який повертає getSnapshot, має бути незмінним. Якщо в базовому сховищі є дані, що змінюються, поверніть новий незмінний знімок, якщо дані було змінено. В іншому випадку поверніть кешований останній знімок.

  • Якщо під час повторного рендерингу буде передано іншу функцію subscribe, React повторно підпишеться на магазин, використовуючи щойно передану функцію subscribe. Ви можете запобігти цьому, оголосивши subscribe поза компонентом.

  • Якщо сховище мутується під час неблокуючого оновлення переходу, React повернеться до виконання цього оновлення як блокуючого. Зокрема, для кожного оновлення переходу React буде викликати getSnapshot вдруге безпосередньо перед застосуванням змін до DOM. Якщо він повертає інше значення, ніж під час першого виклику, React перезапустить оновлення з нуля, цього разу застосувавши його як блокуюче, щоб гарантувати, що кожен компонент на екрані відображає ту саму версію магазину.

  • Не рекомендується призупиняти рендеринг на основі значення, що повертається useSyncExternalStore. Причина у тому, що мутації зовнішнього сховища не можна позначити як неблокуючі оновлення переходів, тому вони викликатимуть найближчий Suspenseзапасний варіант, замінюючи вже відрендерений вміст на екрані на завантажувальний спінер, що зазвичай призводить до поганого UX.

    Наприклад, не рекомендується наступне:

    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-компонентів будуть читати дані лише зі своїх пропсів, стану,та контексту.Втім, інколи компоненту потрібно прочитати деякі дані зі сховища за межами React, які змінюються з часом. До таких випадків належать:

  • Сторонні бібліотеки керування станом, які зберігають стан поза React.
  • Браузерні API, які надають змінне значення та події для підписки на його зміни.

Викличте useSyncExternalStore на верхньому рівні вашого компонента, щоб прочитати значення із зовнішнього сховища даних.

import { useSyncExternalStore } from 'react';
import { todosStore } from './todoStore.js';

function TodosApp() {
  const todos = useSyncExternalStore(todosStore.subscribe, todosStore.getSnapshot);
  // ...
}

Повертає знімок</CodeStep> даних у сховищі. В якості аргументів потрібно передати дві функції:</p> <ol><li>Функція <CodeStep data-step="1"><code>subscribe повинен підписатися на магазин і повернути функцію, яка скасовує підписку.

  • Функція <код>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>
        </>
      );
    }
    // This is an example of a third-party store
    // that you might need to integrate with React.
    
    // If your app is fully built with React,
    // we recommend using React state instead.
    
    let nextId = 0;
    let todos = [{ id: nextId++, text: 'Todo #1' }];
    let listeners = [];
    
    export const todosStore = {
      addTodo() {
        todos = [...todos, { id: nextId++, text: 'Todo #' + nextId }]
        emitChange();
      },
      subscribe(listener) {
        listeners = [...listeners, listener];
        return () => {
          listeners = listeners.filter(l => l !== listener);
        };
      },
      getSnapshot() {
        return todos;
      }
    };
    
    function emitChange() {
      for (let listener of listeners) {
        listener();
      }
    }

    За можливості, ми рекомендуємо використовувати вбудований стан 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 знає, як читати значення із зовнішнього 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);
      };
    }

    Винесення логіки у користувацький хук

    Зазвичай ви не будете писати 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 />
        </>
      );
    }
    import { useSyncExternalStore } from 'react';
    
    export function useOnlineStatus() {
      const isOnline = useSyncExternalStore(subscribe, getSnapshot);
      return isOnline;
    }
    
    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);
      };
    }

    Додано підтримку серверного рендерингу

    Якщо ваш 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 викликається після кожного повторного рендерингу

    Цю функцію підписки визначено всередині компонента, тому вона відрізняється у кожному рендерингу:

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