useSyncExternalStore
useSyncExternalStore — хук React для подписки на внешние источники данных. Используется библиотеками состояния и для подписки на браузерные API.
Зачем нужен
Когда данные живут вне React (например, в глобальном объекте или браузерном API), обычный useState + useEffect может давать рассинхронизацию при concurrent-рендере. useSyncExternalStore решает это.
Синтаксис
const value = useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot?);
subscribe— функция подписки, возвращает функцию отпискиgetSnapshot— возвращает текущее значениеgetServerSnapshot— для SSR (опционально)
Пример: online status
function subscribe(callback: () => void) {
window.addEventListener('online', callback);
window.addEventListener('offline', callback);
return () => {
window.removeEventListener('online', callback);
window.removeEventListener('offline', callback);
};
}
function getSnapshot() {
return navigator.onLine;
}
function getServerSnapshot() {
return true;
}
function OnlineStatus() {
const isOnline = useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
return <p>{isOnline ? 'Онлайн' : 'Оффлайн'}</p>;
}
Пример: window size
function subscribe(callback: () => void) {
window.addEventListener('resize', callback);
return () => window.removeEventListener('resize', callback);
}
function getSnapshot() {
return window.innerWidth;
}
function Width() {
const width = useSyncExternalStore(subscribe, getSnapshot);
return <p>{width}px</p>;
}
Пример: localStorage
function subscribe(callback: () => void) {
window.addEventListener('storage', callback);
return () => window.removeEventListener('storage', callback);
}
function getSnapshot() {
return localStorage.getItem('theme') ?? 'light';
}
function Theme() {
const theme = useSyncExternalStore(subscribe, getSnapshot);
return <p>{theme}</p>;
}
Стор
Сторы вроде Zustand и Redux внутри используют useSyncExternalStore. Это обеспечивает совместимость с concurrent-режимом React.
const store = {
state: { count: 0 },
listeners: new Set<() => void>(),
subscribe(listener: () => void) {
this.listeners.add(listener);
return () => this.listeners.delete(listener);
},
getSnapshot() {
return this.state;
},
increment() {
this.state = { count: this.state.count + 1 };
this.listeners.forEach((l) => l());
},
};
function Counter() {
const state = useSyncExternalStore(
store.subscribe.bind(store),
store.getSnapshot.bind(store)
);
return (
<button onClick={() => store.increment()}>{state.count}</button>
);
}
Важно
getSnapshotдолжен возвращать тот же объект, если данные не изменились- Иначе React уйдёт в бесконечный цикл
- Если данные — объект, кешируй его
Итоги
useSyncExternalStore— для внешних источников- Подписка и чтение значения
- Совместим с concurrent-режимом
- Используется библиотеками состояния
getSnapshotдолжен быть стабильным