Що робить хук useSyncExternalStore()?
Просте визначення
useSyncExternalStore()- це хук, який дозволяє React підписуватися на зовнішній стан (тобто стан, що зберігається поза React), і гарантує узгоджене й передбачуване оновлення UI, навіть якщо використовується конкурентний рендеринг або серверний рендеринг (SSR).
Синтаксис
const snapshot = useSyncExternalStore(
subscribe,
getSnapshot,
getServerSnapshot? // опційно для SSR
);| Аргумент | Тип | Призначення |
|---|---|---|
subscribe | fn((callback) => unsubscribe) | Функція, яка підписує компонент на зміни в зовнішньому сховищі |
getSnapshot | fn() => any | Повертає поточний стан (snapshot) зі сховища |
getServerSnapshot (опційно) | fn() => any | Використовується при серверному рендерингу, щоб отримати стан без доступу до клієнта |
Приклад використання
Припустимо, у нас є просте зовнішнє сховище:
// 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-компонента:
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 гарантує, що:
- Під час рендеру React викликає
getSnapshot()і використовує його значення. - Якщо під час рендеру зовнішній стан змінюється, React повторює рендер, щоб відобразити актуальні дані.
- Після монтування React викликає
subscribe(listener)і стежить за змінами. - Коли стан у зовнішньому сторі змінюється, React викликає listener,
перевіряє новий
getSnapshot(), і якщо він відрізняється, запускає оновлення.
Головне: React робить це синхронно, щоб уникнути "розсинхронізації" між даними і UI.
Приклад з Redux (внутрішньо)
React-Redux v8+ тепер внутрішньо використовує useSyncExternalStore()
замість кастомних підписок, щоб гарантувати коректну поведінку в React 18:
const state = useSyncExternalStore(
store.subscribe,
store.getState,
store.getState // для SSR
);Раніше Redux-хук useSelector покладався на useEffect → могли бути лаги і невідповідності.
Тепер усе синхронно і передбачувано.
Приклад з Window Size (зовнішнє джерело - браузер)
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): він дозволяє задати стан на сервері, якщо клієнтські дані недоступні.
Приклад:
useSyncExternalStore(
store.subscribe,
store.getSnapshot,
() => 0 // на сервері вважаємо, що count = 0
);Це запобігає помилкам гідратації:
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, кастомні стори |
Коротка відповідь
Для співбесідиКоротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.