Запропонувати правкуПокращити цю статтюДопрацюйте відповідь до «Що робить хук useSyncExternalStore()?». Ваші зміни проходять модерацію перед публікацією.Потрібне підтвердженняКонтентЩо ви змінюєте🇺🇸EN🇺🇦UAПереглядЗаголовок (UA)Коротка відповідь (UA)**`useSyncExternalStore()`** - це хук, який дозволяє React **підписуватися на зовнішній стан** (тобто стан, що зберігається *поза React*), і **гарантує узгоджене й передбачуване оновлення** UI, навіть при конкурентному або серверному рендерингу (SSR). **Ключове:** він читає стан синхронно під час рендеру і повторює рендер, якщо зовнішні дані змінилися, уникаючи розсинхронізації між стором і компонентом.Показується над повною відповіддю для швидкого нагадування.Відповідь (UA)Зображення## Просте визначення > `useSyncExternalStore()` - це хук, який дозволяє React **підписуватися на зовнішній стан** > (тобто стан, що зберігається *поза React*), і **гарантує узгоджене й передбачуване оновлення** UI, > навіть якщо використовується конкурентний рендеринг або серверний рендеринг (SSR). --- ## Синтаксис ```javascript const snapshot = useSyncExternalStore( subscribe, getSnapshot, getServerSnapshot? // опційно для SSR ); ``` | Аргумент | Тип | Призначення | |---|---|---| | `subscribe` | `fn((callback) => unsubscribe)` | Функція, яка підписує компонент на зміни в зовнішньому сховищі | | `getSnapshot` | `fn() => any` | Повертає поточний стан (snapshot) зі сховища | | `getServerSnapshot` *(опційно)* | `fn() => any` | Використовується при серверному рендерингу, щоб отримати стан без доступу до клієнта | --- ## Приклад використання Припустимо, у нас є просте зовнішнє сховище: ```javascript // store.js let listeners = []; let count = 0; export const store = { getSnapshot: () => count, subscribe: (callback) => { listeners.push(callback); return () => { listeners = listeners.filter(l => l !== callback); }; }, increment: () => { count++; listeners.forEach(l => l()); }, }; ``` Тепер підключимо його до React-компонента: ```javascript import { useSyncExternalStore } from 'react'; import { store } from './store'; function Counter() { const count = useSyncExternalStore( store.subscribe, store.getSnapshot ); return ( <div> <p>Count: {count}</p> <button onClick={store.increment}>+</button> </div> ); } ``` Тепер компонент: - **читає стан** із зовнішнього стора; - **автоматично перерендерюється**, коли `store.increment()` змінює стан; - працює **стабільно при concurrent рендерингу і SSR**. --- ## Навіщо з'явився цей хук До React 18 бібліотеки (Redux, Zustand, MobX та ін.) використовували **нестабільні підходи** на кшталт `useEffect` або `useLayoutEffect` для синхронізації із зовнішнім станом. Це викликало проблеми: - При **Concurrent Rendering** React міг почати рендер із "застарілими" даними; - При **SSR / Hydration** стан клієнта міг не збігтися з серверним → попередження; - Підписка могла викликатися в неправильний момент життєвого циклу. `useSyncExternalStore()` вирішує всі ці проблеми. --- ## Як це працює під капотом React гарантує, що: 1. **Під час рендеру** React викликає `getSnapshot()` і використовує його значення. 2. Якщо під час рендеру зовнішній стан змінюється, React **повторює рендер**, щоб відобразити актуальні дані. 3. Після монтування React викликає `subscribe(listener)` і стежить за змінами. 4. Коли стан у зовнішньому сторі змінюється, React викликає listener, перевіряє новий `getSnapshot()`, і якщо він відрізняється, запускає оновлення. > Головне: React робить це **синхронно**, щоб уникнути "розсинхронізації" між даними і UI. --- ## Приклад з Redux (внутрішньо) React-Redux v8+ тепер **внутрішньо** використовує `useSyncExternalStore()` замість кастомних підписок, щоб гарантувати коректну поведінку в React 18: ```javascript const state = useSyncExternalStore( store.subscribe, store.getState, store.getState // для SSR ); ``` Раніше Redux-хук `useSelector` покладався на `useEffect` → могли бути лаги і невідповідності. Тепер усе синхронно і передбачувано. --- ## Приклад з Window Size (зовнішнє джерело - браузер) ```javascript function useWindowWidth() { return useSyncExternalStore( (callback) => { window.addEventListener('resize', callback); return () => window.removeEventListener('resize', callback); }, () => window.innerWidth ); } function Component() { const width = useWindowWidth(); return <div>Ширина вікна: {width}px</div>; } ``` Навіть якщо React "призупинить" рендер у Concurrent Mode, `useSyncExternalStore` гарантує, що `width` завжди відповідає **актуальному стану вікна**, без "старих" значень або глітчів при гідратації. --- ## Що робить третій аргумент (`getServerSnapshot`) Цей аргумент використовується при **серверному рендерингу (SSR)**: він дозволяє задати *стан на сервері*, якщо клієнтські дані недоступні. Приклад: ```javascript useSyncExternalStore( store.subscribe, store.getSnapshot, () => 0 // на сервері вважаємо, що count = 0 ); ``` Це запобігає помилкам гідратації: ```javascript Warning: Text content did not match. Server: "0" Client: "5" ``` --- ## Важливо: не плутати з useEffect або useLayoutEffect | Хук | Коли оновлюється | Підходить для зовнішнього стану? | |---|---|---| | `useEffect` | Асинхронно, після рендеру | Може запізнюватися | | `useLayoutEffect` | Синхронно після commit | Краще, але не SSR-сумісний | | `useSyncExternalStore` | Синхронно в самому React-циклі | Ідеальний варіант | --- ## Підсумок > `useSyncExternalStore()` - це низькорівневий хук, > який React використовує для **синхронізації UI із зовнішніми джерелами даних**. > > Він гарантує: > > - коректну роботу при **Concurrent Rendering**, > - безпеку при **SSR + Hydration**, > - відсутність "розсинхронізації" між стором і компонентом. --- ### Коротко | Властивість | Значення | |---|---| | Повертає | Поточний стан із зовнішнього джерела | | Автоматично підписується | Так | | SSR-сумісний | Так | | Concurrent-безпечний | Так | | Замінює старі патерни (`useEffect` для підписок) | Так | | Основне застосування | Redux, Zustand, MobX, event listeners, кастомні стори |Для рев’юераПримітка для модератора (необов’язково)Бачить лише модератор. Прискорює рев’ю.