Урок 07

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 должен быть стабильным