В React 18 появился хук useSyncExternalStore, который позволяет компонентам считывать данные из любого источника вне дерева React — браузерных API, WebSockets или сторонних хранилищ — без визуальных артефактов, которые возникали при использовании старого паттерна дублирования данных через useState и useEffect. Если вы когда-нибудь видели мерцание интерфейса из-за того, что одна его часть показывала старый размер окна, а другая уже отображала новый, этот хук — именно то решение, которое вы ждали.
Почему внешнему состоянию нужен новый хук
Компоненты React всегда могли подписываться на объекты, находящиеся за пределами их области видимости: объект window, хранилище Redux или WebSocket-соединение. Традиционный подход заключался в создании подписки внутри useEffect, передаче входящих значений в локальное состояние через setState и запуске рендеринга React. Это отлично работает при синхронном рендеринге, но в React 18 появился конкурентный (concurrent) рендеринг, при котором компонент может рендериться несколько раз до того, как браузер фактически отрисует изменения на экране. В этом режиме паттерн «зеркалирования в локальное состояние» может привести к tearing (разрыву целостности) — ситуации, когда разные части интерфейса считывают разные снимки (snapshots) одного и того же внешнего значения в рамках одного цикла рендеринга.
Хук useSyncExternalStore был создан именно для предотвращения tearing. Он запрашивает у внешнего источника snapshot (текущее значение) и функцию subscribe (способ получения уведомлений об изменениях). React вызывает subscribe при монтировании компонента и автоматически отписывается при размонтировании. Как только snapshot, возвращаемый функцией getSnapshot, изменяется, React планирует рендеринг, который считывает один и тот же snapshot для всего дерева компонентов, гарантируя согласованность данных.
Две необходимые функции
| Функция | Что она делает |
|---|---|
| subscribe | Принимает callback, который должен вызываться при каждом изменении внешнего значения. Возвращает функцию unsubscribe, которую React вызовет при очистке. |
| getSnapshot | Возвращает текущее значение из внешнего источника. Она должна быть стабильной по ссылке (referentially stable) — возвращать один и тот же объект функции при каждом рендеринге, чтобы React мог корректно сравнивать снимки. |
Минимальная реализация для проверки статуса online в браузере выглядит так:
function useOnlineStatus() {
return useSyncExternalStore(
(onChange) => {
window.addEventListener('online', onChange);
window.addEventListener('offline', onChange);
return () => {
window.removeEventListener('online', onChange);
window.removeEventListener('offline', onChange);
};
},
() => navigator.onLine
);
}
Когда любой компонент вызывает useOnlineStatus(), React будет перерисовывать его только тогда, когда значение navigator.onLine действительно изменится, и при каждом рендеринге будет отображаться одно и то же значение.
Правила, которые помогут избежать проблем с хуком
Возвращайте минимально необходимый объем данных.
Если внешнее хранилище содержит большой объект, а компоненту нужно только одно поле, возвращайте только это поле. Возврат слишком больших объектов вызывает лишние рендеринги.Мемоизируйте
getSnapshot.
Если вы будете пересоздавать функцию при каждом рендеринге, React будет воспринимать её как новый источник, что может привести к бесконечному циклу рендеринга. Оберните её вuseCallbackили определите вне компонента.Избегайте возврата новых объектов при каждом вызове.
Возврат нового объекта (например,{ count: store.getCount() }) создает новую ссылку при каждом рендеринге. Из-за этого React решит, что snapshot изменился, и запустит бесконечный цикл рендеринга. Используйте примитивы, строки, числа или мемоизированные объекты.Не используйте его для внутреннего состояния компонента.
useStateпо-прежнему является правильным инструментом для данных, которые живут только внутри компонента.useSyncExternalStoreсоздает дополнительные накладные расходы, которые не нужны для локального состояния.
Когда стоит его использовать
- Браузерные API — размер окна, медиазапросы,
navigator.onLine, статус батареи. - Независимые от фреймворка хранилища — Zustand, Redux, MobX или любое кастомное хранилище с API подписки.
- Изменяемые источники, обновляющиеся вне React — сообщения WebSocket, события изменений в IndexedDB, уведомления Service Worker.
Если ваш источник данных уже находится внутри React (например, состояние родительского компонента), используйте useState или Context API.
Что говорит сообщество
Первые пользователи отмечают, что useSyncExternalStore устраняет мерцание, которое наблюдалось при изменении размера окон в режиме конкурентного рендеринга. Некоторые библиотеки уже перевели свои внутренние хуки на этот API, обещая более предсказуемое поведение в разных версиях React. Обратной стороной является чуть большая когнитивная нагрузка: разработчикам нужно следить за стабильностью двух функций и избегать возврата новых объектов при каждом вызове.
Что изучить дальше
Предстоящие релизы React могут ужесточить контракт вокруг useSyncExternalStore, возможно, добавив встроенные вспомогательные функции для распространенных браузерных API. Следите за официальным блогом React на предмет возможного устаревания старого паттерна, имитирующего useEffect. Тем временем хук является стабильным и входит в состав публичного API, поэтому вы можете безопасно проводить рефакторинг существующего кода, работающего с внешним состоянием.
Быстрый тест-драйв
Живое демо, демонстрирующее синхронизацию размера окна, определение статуса online/offline и простое хранилище Zustand, доступно по адресу https://usesyncexternalstore.vercel.app/. Исходный код с комментариями доступен по ссылке https://github.com/dev48v/usesyncexternalstore.
Итог: useSyncExternalStore предоставляет React надежный мост к любым изменяемым данным, находящимся вне цикла рендеринга, предотвращая tearing (разрыв визуального состояния) и лишние рендеры. Используйте его для внешних источников, делайте снапшоты минимальными и стабильными, и позвольте React взять на себя основную работу по обеспечению согласованности данных.
