Більшість стану живе в React (useState, useReducer). Але інколи дані зберігаються зовні: у сторонній бібліотеці стану, в API браузера (navigator.onLine, matchMedia, localStorage), у власному класі-сховищі.
Наївний підхід - useEffect з підпискою і копією в useState - має проблеми в конкурентному рендерингу: React може рендерити частини дерева в різний час, і різні компоненти побачать різні версії зовнішнього значення («розрив», tearing).
useSyncExternalStore - офіційний спосіб читати зовнішнє джерело узгоджено:
import { useSyncExternalStore } from 'react';
function subscribe(callback) {
window.addEventListener('online', callback);
window.addEventListener('offline', callback);
return () => {
window.removeEventListener('online', callback);
window.removeEventListener('offline', callback);
};
}
export function useOnlineStatus() {
return useSyncExternalStore(
subscribe,
() => navigator.onLine, // знімок на клієнті
() => true, // знімок для серверного рендеру
);
}
Три аргументи:
subscribe(callback)- підписатися на зміни й повернути функцію відписки;getSnapshot()- поточне значення;getServerSnapshot()- значення для серверного рендеру й гідратації (на сервері немаєnavigator).
Ключові правила:
getSnapshotмає повертати те саме значення, доки дані не змінилися. Якщо він щоразу створює новий об'єкт (() => ({ ...store.state })), React вважатиме, що дані змінилися на кожному рендері, - нескінченний цикл і помилка. Повертайте збережене незмінне значення або примітив;subscribe- стабільна функція, оголошена поза компонентом (чи вuseCallback), інакше React перепідписуватиметься на кожному рендері;- оновлення з такого джерела не можна позначити як transition - вони завжди синхронні.
Хто вже використовує: Redux (useSelector), Zustand, TanStack Query та інші бібліотеки стану побудовані на ньому. У застосунку цей хук зазвичай потрібен для API браузера чи власного сховища.
Приклад із localStorage: підписка на подію storage (зміни з інших вкладок) плюс власна подія для змін у поточній вкладці - і стан синхронізований між вкладками.