UseSyncExternalStore дозволяє компоненту читати дані, що знаходяться поза межами React, не ризикуючи візуальними збоями, які може спричинити конкурентний рендеринг. Він замінює старий патерн перенесення зовнішніх значень у локальний стан за допомогою useEffect — патерн, який може спричинити «tearing» (розрив) — ситуацію, коли частини інтерфейсу відображають різні значення під час одного рендерингу.
Чому старий патерн не працює під час конкурентного рендерингу
До React 18 типовим рішенням було:
const [value, setValue] = useState(initial);
useEffect(() => {
const unsubscribe = externalSource.subscribe(v => setValue(v));
return unsubscribe;
}, []);
Ефект спрацьовує після того, як компонент відрендерився. Коли конкурентний рендерер React вирішує поставити рендеринг на паузу, перервати або повторити його, ефект може спрацювати в інший момент, ніж UI, який читає стан. Результатом є невідповідність: один компонент міг прочитати старе значення, інший — нове, і екран на мить показує несумісні дані. Цей «tearing» є нешкідливим у суто синхронному світі, але стає помітною помилкою, коли React рендерить кілька версій інтерфейсу паралельно.
Як useSyncExternalStore вирішує цю проблему
Хук потребує два колбеки:
- subscribe – реєструє слухача та повертає функцію для його видалення.
- getSnapshot – повертає поточне значення зовнішнього джерела.
React викликає subscribe, коли компонент монтується, і видаляє його при розмонтуванні. Під час кожного рендерингу він викликає getSnapshot і гарантує, що всі компоненти, які використовують той самий зовнішній стор, читають абсолютно однаковий знімок (snapshot). Оскільки читання відбувається на фазі рендерингу, рендерер може координувати оновлення в усьому дереві, усуваючи tearing.
Бібліотеки, такі як Zustand та Redux, вже обгортають свої стори цим хуком, тому вони працюють «з коробки» з конкурентними можливостями React 18.
Правила, щоб хук працював правильно
- Повертайте лише те, що потрібно компоненту – великий об'єкт змушує React порівнювати глибокі структури при кожному оновленні. Вузький snapshot робить виявлення змін дешевим.
- Зберігайте референційну ідентичність – якщо зовнішнє значення не змінилося, getSnapshot має повертати те саме посилання на об'єкт. Повернення нового об'єкта при кожному виклику призведе до нескінченного циклу рендерингу, оскільки React бачитиме зміну при кожному рендерингу.
- Обмежуйте використання лише зовнішніми джерелами – локальний стан компонента має належати до
useStateабоuseReducer. Використання хука для зовнішнього стора для суто внутрішніх значень додає непотрібну непряму логіку.
Коли варто використовувати useSyncExternalStore
- Обгортання браузерних API (розмір вікна, статус мережі, медіа-запити).
- Підключення звичайного JavaScript-стора, який не знає про React.
- Заміна prop-drilling глобальним джерелом лише для читання, на яке підписано багато компонентів.
Контраргумент
Патерн useState + useEffect все ще працює для простих даних, що рідко змінюються, особливо якщо додаток не використовує конкурентні можливості. Це підхід нижчого рівня, який деяким розробникам легше зрозуміти, оскільки логіка підписки явно знаходиться в ефекті. Однак компромісом є ризик виникнення tearing, коли вмикається конкурентний рендеринг. Для проєктів, які вже використовують нові можливості React 18, додаткова безпека useSyncExternalStore переважає мінімальний обсяг додаткового шаблонного коду.
На що звернути увагу далі
- Переконайтеся, що getSnapshot вашого стора повертає стабільне посилання; швидкий
console.logпосилання під час розробки може виявити випадкове перестворення об'єкта. - Тримайте функцію підписки легкою — важка робота має відбуватися всередині зовнішнього джерела, а не в колбеку, що передається React.
- Тестуйте компоненти в конкурентному режимі React (наприклад, використовуючи
actзReactDOM.createRoot), щоб підтвердити відсутність tearing.
Живе демо показує хук у дії: синхронізацію простого лічильника, що зберігається поза React, та оновлення кількох компонентів без візуальних збоїв. Демо доступне за посиланням https://usesyncexternalstore.vercel.app/. Вихідний код знаходиться на GitHub, а розширений матеріал пояснює деталі реалізації.
Висновок: У світі, де React може рендерити частини інтерфейсу не по порядку, useSyncExternalStore є надійним містком між зовнішніми даними та рендерингом компонентів. Він зберігає візуальну цілісність, масштабується до глобальних сторів і вже вбудований у популярні бібліотеки управління станом. Використовуйте його всюди, де вам потрібно читати стан поза React, і тримайте snapshot маленькими та стабільними, щоб отримати від нього максимальну користь.
