Запропонувати правкуПокращити цю статтюДопрацюйте відповідь до «Що робить `useSyncExternalStore()`?». Ваші зміни проходять модерацію перед публікацією.Потрібне підтвердженняКонтентЩо ви змінюєте🇺🇸EN🇺🇦UAПереглядЗаголовок (UA)Коротка відповідь (UA)**`useSyncExternalStore()`** підключає React-компонент до зовнішнього стану, яким не керує React (наприклад, Redux, Zustand, EventEmitter, кастомний store, window.location тощо). **Ключове:** він гарантує, що React завжди читає поточний знімок стану і оновлюється синхронно, якщо store змінюється під час рендеру.Показується над повною відповіддю для швидкого нагадування.Відповідь (UA)Зображення## Що робить `useSyncExternalStore()` `useSyncExternalStore()` підключає React-компонент до **зовнішнього стану**, яким **не керує React** (наприклад, Redux, Zustand, EventEmitter, кастомний store, window.location тощо). Він забезпечує: - **синхронне читання поточного значення** (знімок store), - **підписку на оновлення**, - **коректну роботу з concurrent rendering** (React 18) і **SSR**. > Простіше кажучи: > `useSyncExternalStore()` дозволяє React-компонентам **надійно читати дані із зовнішнього сховища** > і оновлюватися при їхній зміні. --- ## Синтаксис ```javascript const state = useSyncExternalStore( subscribe, // функція підписки getSnapshot, // функція читання поточного стану getServerSnapshot? // (опційно) версія для SSR ); ``` --- ## Приклад: підключення до кастомного store Припустимо, у нас є просте сховище поза React: ```javascript // store.js let listeners = []; let count = 0; export function increment() { count++; listeners.forEach((l) => l()); } export function subscribe(listener) { listeners.push(listener); return () => { listeners = listeners.filter((l) => l !== listener); }; } export function getSnapshot() { return count; } ``` Тепер підключимо його до React через `useSyncExternalStore`: ```javascript import { useSyncExternalStore } from "react"; import { subscribe, getSnapshot, increment } from "./store"; function Counter() { const count = useSyncExternalStore(subscribe, getSnapshot); return ( <div> <p>Лічильник: {count}</p> <button onClick={increment}>+1</button> </div> ); } ``` Тепер: - Компонент **одразу** отримує актуальне значення з `getSnapshot()`; - При зміні store викликається `subscribe()` -> React перерендерює компонент; - Усе працює **синхронно і передбачувано**, навіть у concurrent-режимі. --- ## Що робить кожен аргумент | Аргумент | Що робить | |---|---| | `subscribe(listener)` | Реєструє колбек, який викликається при зміні стану; повертає функцію відписки | | `getSnapshot()` | Повертає **поточне значення** стану із зовнішнього джерела | | `getServerSnapshot()` *(опційно)* | Версія `getSnapshot` для **SSR**, щоб сервер і клієнт були синхронізовані | --- ## Чому це з'явилося в React 18 Раніше для цього використовували кастомні хуки на кшталт `useStore`, `useSelector` тощо. Але з появою **Concurrent Rendering** (паралельний рендер) React почав рендерити кілька версій UI "одночасно". Проблема старих рішень: - Вони могли отримати "старе" значення зі store, - Або спричинити "розсинхронізацію" (UI показував не те, що реально в store). `useSyncExternalStore` вирішує це, тому що: - Гарантує, що React завжди читає **поточний знімок** стану, - І **оновлюється синхронно**, якщо store змінюється під час рендеру. --- ## Приклад: інтеграція з Redux Redux 8+ тепер **внутрішньо** використовує `useSyncExternalStore`. ```javascript import { useSelector } from "react-redux"; function MyComponent() { const count = useSelector((state) => state.counter.value); return <div>{count}</div>; } ``` Усередині `useSelector` реалізовано приблизно так: ```javascript function useSelector(selector) { return useSyncExternalStore( store.subscribe, () => selector(store.getState()) ); } ``` --- ## Приклад з window.location (зовнішнє джерело) ```javascript function useLocation() { return useSyncExternalStore( (callback) => { window.addEventListener("popstate", callback); return () => window.removeEventListener("popstate", callback); }, () => window.location.pathname ); } function RouterView() { const path = useLocation(); return <div>Поточний шлях: {path}</div>; } ``` React автоматично оновить компонент, коли зміниться `window.location`. --- ## SSR-версія (`getServerSnapshot`) При серверному рендерингу (Next.js, Remix тощо) можна передати 3-й аргумент: ```javascript useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot); ``` Це важливо, щоб **значення на сервері і клієнті збігалося**, інакше React видасть попередження про "mismatched hydration". --- ## Коли використовувати | Коли | Чому | |---|---| | Підключаєшся до **зовнішнього сховища** (Redux, Zustand, кастомний store) | React не керує цим станом | | Потрібна **реактивна підписка** | Компонент повинен перерендеритися при зміні store | | Важливо уникнути розсинхронізації в Concurrent Rendering | `useSyncExternalStore` гарантує узгодженість | | Потрібна безпечна поведінка при SSR | Можна передати `getServerSnapshot` | --- ## Коли **не** потрібно | Випадок | Чому | |---|---| | Ти працюєш зі станом через `useState` або `useReducer` | React сам керує даними - `useSyncExternalStore` не потрібен | | Ти робиш API-запити | Використовуй `useEffect` / React Query | | Сховище локальне, не глобальне | Надлишково | --- ## Підсумок | Питання | Відповідь | |---|---| | Що робить `useSyncExternalStore()` | Дозволяє React-компоненту підписатися на зовнішній стан (store) і отримувати актуальні дані | | Аргументи | `(subscribe, getSnapshot, getServerSnapshot?)` | | Навіщо потрібен | Для узгодженості стану при Concurrent Rendering і SSR | | Де використовується | Redux, Zustand, Jotai, кастомні store, event emitters | | Не використовувати | Для локального React-стану (`useState`, `useReducer`) | --- **Головна ідея:** > `useSyncExternalStore()` - це "офіційний спосіб" синхронно підключити React до зовнішніх станів > без ризику розсинхронізації між store і UI.Для рев’юераПримітка для модератора (необов’язково)Бачить лише модератор. Прискорює рев’ю.