useSyncExternalStore позволяет компоненту считывать данные, находящиеся за пределами React, не рискуя получить визуальные артефакты, которые может вызвать конкурентный рендеринг. Он заменяет старый паттерн переноса внешних значений в локальное состояние с помощью useEffect — паттерн, который может привести к «разрыву» (tearing), когда части интерфейса отображают разные значения во время одного и того же рендеринга.

Почему старый паттерн ломается при конкурентном рендеринге

До React 18 стандартным рецептом было:

const [value, setValue] = useState(initial);
useEffect(() => {
  const unsubscribe = externalSource.subscribe(v => setValue(v));
  return unsubscribe;
}, []);

Эффект запускается после того, как компонент отрендерился. Когда конкурентный рендерер React решает приостановить, прервать или повторно запустить рендеринг, эффект может сработать в другой момент, нежели UI, который считывает состояние. Результат — несоответствие: один компонент может считать старое значение, другой — новое, и экран на мгновение покажет противоречивые данные. Этот «разрыв» безобиден в чисто синхронном мире, но становится видимым багом, когда React рендерит несколько версий интерфейса параллельно.

Как useSyncExternalStore решает проблему

Хук требует два колбэка:

  • subscribe — регистрирует слушателя и возвращает функцию для его удаления.
  • getSnapshot — возвращает текущее значение внешнего источника.

React вызывает subscribe при монтировании компонента и вызывает функцию очистки при размонтировании. Во время каждого рендеринга он вызывает getSnapshot и гарантирует, что все компоненты, использующие один и тот же внешний стор, считывают один и тот же снапшот. Поскольку чтение происходит на фазе рендеринга, рендерер может координировать обновления по всему дереву, устраняя «разрыв».

Библиотеки, такие как Zustand и Redux, уже оборачивают свои сторы этим хуком, поэтому они работают «из коробки» с конкурентными функциями React 18.

Правила, которые не дадут хуку работать неправильно

  1. Возвращайте только то, что нужно компоненту — большой объект заставляет React сравнивать глубокие структуры при каждом обновлении. Узкий снапшот делает обнаружение изменений дешевым.
  2. Сохраняйте ссылочную идентичность — если внешнее значение не изменилось, getSnapshot должен возвращать ту же самую ссылку на объект. Возврат нового объекта при каждом вызове вызывает бесконечный цикл рендеринга, так как React видит изменения при каждом рендере.
  3. Используйте только для внешних источников — локальное состояние компонента должно находиться в useState или useReducer. Использование хука для внешнего стора для чисто внутренних значений добавляет ненужную косвенность.

Когда стоит использовать useSyncExternalStore

  • Обертка браузерных API (размер окна, статус сети, медиа-запросы).
  • Подключение обычного JavaScript-стора, который ничего не знает о React.
  • Замена проп-дриллинга (prop-drilling) глобальным источником только для чтения, на который подписано множество компонентов.

Контраргумент

Паттерн useState + useEffect все еще работает для простых, редко меняющихся данных, особенно если в приложении не включены конкурентные функции. Это более низкоуровневый подход, который некоторым разработчикам проще понять, так как логика подписки явно прописана в эффекте. Однако компромиссом является риск «разрыва» при включении конкурентного рендеринга. Для проектов, которые уже используют новые возможности React 18, дополнительная безопасность useSyncExternalStore перевешивает минимальное количество дополнительного шаблонного кода.

На что обратить внимание

  • Убедитесь, что getSnapshot вашего стора возвращает стабильную ссылку; быстрый console.log ссылки во время разработки может выявить случайное пересоздание объекта.
  • Держите функцию подписки легковесной — тяжелые операции должны происходить внутри внешнего источника, а не в колбэке, передаваемом в React.
  • Тестируйте компоненты в конкурентном режиме React (например, используя act с ReactDOM.createRoot), чтобы подтвердить отсутствие «разрывов».

Живое демо показывает работу хука в действии: синхронизацию простого счетчика, хранящегося вне React, и обновление нескольких компонентов без визуальных артефактов. Демо доступно по адресу https://usesyncexternalstore.vercel.app/. Исходный код находится на GitHub, а более подробная статья объясняет детали реализации.

Итог: В мире, где React может рендерить части интерфейса не по порядку, useSyncExternalStore служит надежным мостом между внешними данными и рендерингом компонентов. Он сохраняет визуальную согласованность, масштабируется до глобальных сторов и уже встроен в популярные библиотеки управления состоянием. Используйте его везде, где вам нужно считывать состояние вне React, и старайтесь делать снапшоты маленькими и стабильными, чтобы получить максимум преимуществ.