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);
// ...
}
Повертає знімок даних у сховищі. Вам потрібно передати дві функції в якості аргументів:
>Ви повинні передати дві функції в якості аргументів:- Функція
subscribe
повинна підписуватися на сховище і повертати функцію, яка відписується. - Функція
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);
// ...
}
Повертає
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]);
// ...
}