Запропонувати правкуПокращити цю статтюДопрацюйте відповідь до «Гідратація React та рендеринг на стороні сервера (SSR)». Ваші зміни проходять модерацію перед публікацією.Потрібне підтвердженняКонтентЩо ви змінюєте🇺🇸EN🇺🇦UAПереглядЗаголовок (UA)Коротка відповідь (UA)**React гідратація (hydration)** - процес, коли React прикріплює обробники подій і стан до HTML, згенерованого на сервері, без перебудови DOM. SSR надсилає готовий HTML для швидкого першого відображення, потім `hydrateRoot()` підключає інтерактивність. ```tsx const html = renderToString(<App />); // Сервер: чистий HTML, без обробників hydrateRoot(document.getElementById('root'), <App />); // Клієнт: прикріплює обробники ``` **Ключове:** якщо сервер і клієнт рендерять різний HTML, React виявляє невідповідність (hydration mismatch) і виконує повний повторний рендеринг.Показується над повною відповіддю для швидкого нагадування.Відповідь (UA)Зображення**React гідратація (hydration)** - процес, коли React прикріплює обробники подій і стан до HTML, згенерованого на сервері, без перебудови DOM. Браузер використовує готову розмітку від сервера, а не будує все з нуля. ## Теорія ### TL;DR - SSR - це як ресторан, де страву подають уже готовою. Гідратація - це коли офіціант каже "можна їсти". CSR - це коли готуєш прямо за столиком. - SSR надсилає готовий HTML одразу (швидкий перший рендер), потім React його "пробуджує". CSR надсилає порожню сторінку і React будує DOM у браузері. - Гідратація падає, якщо HTML сервера не збігається з тим, що React очікує на клієнті. Тоді React робить повний повторний рендеринг. - SSR для публічних сайтів, де важливі SEO і швидкість першого відображення. CSR для внутрішніх інструментів, де це не має значення. ### Швидкий приклад ```tsx // Сервер (Node.js + React) import { renderToString } from 'react-dom/server'; const html = renderToString(<App />); // Результат: <div><button>Count: 0</button></div> // Обробників подій ще немає. // Браузер отримує і одразу відображає HTML. // Потім React гідратує: import { hydrateRoot } from 'react-dom/client'; hydrateRoot(document.getElementById('root'), <App />); // React обходить дерево, знаходить існуючі DOM-вузли, прикріплює onClick. // Новий DOM не створюється - використовується серверний HTML. ``` React обходить дерево компонентів двічі: на сервері для генерації HTML, на клієнті для зіставлення і прикріплення обробників. Перебудови DOM не відбувається. ### Ключова різниця: SSR і CSR SSR надсилає готовий HTML рядок. Браузер відображає його одразу, поки React завантажується у фоні. Гідратація потім "пробуджує" статичну розмітку, прикріплюючи обробники подій і стан. CSR надсилає порожню HTML-оболонку і React будує весь DOM у браузері. Простіше в налаштуванні, але повільніше до першого відображення. Компроміс конкретний: SSR витрачає CPU сервера на кожен запит. CSR витрачає час користувача при кожному першому відвідуванні. ### Коли що використовувати - **SSR + гідратація:** публічні сайти, e-commerce, блоги, контент де важливий SEO, користувачі з повільним інтернетом - **Тільки CSR:** внутрішні дашборди, адмін-панелі, real-time інструменти де SEO не потрібен - **Гібридний підхід (Islands):** великі сайти з переважно статичним контентом і кількома інтерактивними секціями. Astro реалізує це нативно: SSR статичних частин, гідратація тільки інтерактивних компонентів. ### Порівняння підходів рендерингу | Підхід | Коли генерується HTML | First Contentful Paint | SEO | |---|---|---|---| | CSR | У браузері | Повільний | Поганий | | SSR | На сервері при кожному запиті | Швидкий | Відмінний | | SSG | Під час збірки | Найшвидший | Відмінний | | ISR | Збірка + ревалідація | Швидкий + свіжий | Відмінний | | Islands | Сервер (статика) + клієнт (інтерактив) | Швидкий | Відмінний | ### Як гідратація працює всередині `renderToString(<App />)` на сервері обходить дерево компонентів і генерує HTML рядок без обробників подій і стану. Браузер отримує це і відображає одразу. Коли запускається `hydrateRoot()`, React знову обходить те саме дерево. Але замість створення DOM-вузлів він порівнює очікуваний вивід компонентів з існуючими елементами і прикріплює обробники, ініціалізує стан, налаштовує підписки. Якщо обидва дерева збігаються, гідратація завершується без попереджень. Якщо ні, React виявляє невідповідність (hydration mismatch), логує попередження в development і повертається до повного повторного рендерингу. React 18 додав два покращення. Вибіркова гідратація (selective hydration) дозволяє різним частинам сторінки гідратуватись незалежно через `Suspense` межі. Якщо користувач натискає на секцію, яка ще не гідратувалась, React пріоритизує її. Streaming SSR надсилає HTML частинами у міру готовності даних, тому браузер починає рендерити ще до отримання повної відповіді. ### Вибіркова гідратація (React 18) ```tsx import { Suspense } from 'react'; function Page() { return ( <div> <Header /> <Suspense fallback={<Spinner />}> <HeavySidebar /> {/* Гідратується незалежно */} </Suspense> <Suspense fallback={<Skeleton />}> <Comments /> {/* Може гідратуватись раніше HeavySidebar */} </Suspense> </div> ); } // Якщо користувач натисне Comments до завершення гідратації, React пріоритизує її. ``` ### Типові помилки **Динамічні значення у рендері** ```tsx // Неправильно - сервер і клієнт генерують різний час export default function Clock() { const [time, setTime] = useState(new Date().toLocaleString()); return <div>{time}</div>; } // Правильно - встановлюємо час тільки після монтування export default function Clock() { const [time, setTime] = useState(''); useEffect(() => { setTime(new Date().toLocaleString()); }, []); return <div>{time || 'Завантаження...'}</div>; } ``` Сервер рендерить один timestamp. Браузер генерує трохи інший. Невідповідність. React повторно рендерить з нуля. **Browser API у рендері** ```tsx // Неправильно - window не існує на сервері, одразу крашиться export default function WindowSize() { const [width, setWidth] = useState(window.innerWidth); return <div>Width: {width}</div>; } // Правильно export default function WindowSize() { const [width, setWidth] = useState(0); useEffect(() => { setWidth(window.innerWidth); }, []); return <div>Width: {width || 'Завантаження...'}</div>; } ``` **Дані користувача на клієнті замість сервера** ```tsx // Неправильно - сервер рендерить "Guest", клієнт рендерить "Alice" після useEffect export default function UserGreeting() { const [user, setUser] = useState(null); useEffect(() => { setUser(getCurrentUser()); }, []); return <div>Привіт {user?.name || 'Guest'}</div>; } // Правильно - отримуємо на сервері, передаємо як пропс export async function getServerSideProps() { const user = await getCurrentUser(); return { props: { user } }; } export default function UserGreeting({ user }) { return <div>Привіт {user.name}</div>; } ``` **Припущення, що гідратація миттєва** ```tsx // Користувач може натиснути кнопку до завершення гідратації. // Натискання відбудеться. Нічого не станеться. Жодної помилки у консолі. // Я бачив, як це знижувало конверсію на e-commerce сайтах з великими JS бандлами. export default function Form() { const [hydrated, setHydrated] = useState(false); useEffect(() => { setHydrated(true); }, []); return ( <form onSubmit={handleSubmit}> <button type="submit" disabled={!hydrated}>Відправити</button> </form> ); } ``` **Ненавмисне використання `Math.random()` у рендері** ```tsx // Неправильно - різне значення на сервері і клієнті export default function App() { return <div>{Math.random()}</div>; // Сервер: 0.123, Клієнт: 0.456 - невідповідність } // Прийнятно, якщо навмисно - використовуй suppressHydrationWarning тільки коли розумієш чому export default function App() { return <div suppressHydrationWarning>{Math.random()}</div>; } ``` ### Де використовується - **Next.js:** `getServerSideProps` запускає SSR; `hydrateRoot` виконується автоматично в браузері - **Remix:** всі маршрути SSR за замовчуванням, гідратація автоматична - **Astro:** SSR статичного HTML, гідратація тільки компонентів з `client:load` - **Gatsby:** пре-рендеринг під час збірки (SSG), гідратація в браузері - **Express + React:** ручний SSR через `renderToString()`, ручна гідратація через `hydrateRoot()` - **SvelteKit:** SSR за замовчуванням, гідратація автоматична ### Питання на співбесіді **Q:** Яка різниця між гідратацією і повторним рендерингом? **A:** Гідратація прикріплює обробники подій до існуючих DOM-вузлів без зміни структури. Повторний рендеринг створює нові DOM-вузли. Гідратація швидка, бо DOM вже є; повторний рендеринг будує все з нуля. **Q:** Що відбувається, якщо HTML сервера не збігається з тим, що очікує React? **A:** React виявляє невідповідність і повертається до повного повторного рендерингу в браузері. Це нівелює переваги SSR. У development режимі побачите попередження в консолі. Типові причини: `Date.now()` або `Math.random()` у рендері, browser-only API, різні дані на сервері і клієнті. **Q:** Чи можна гідратувати тільки частину сторінки? **A:** Так. React 18 це називає вибіркова гідратація (selective hydration). Загорніть секції в `Suspense` і React гідратує їх незалежно. Astro йде далі з islands архітектурою: гідратуються тільки компоненти з `client:load`. React Server Components вирішують це інакше: такі компоненти взагалі не гідратуються, бо не надсилають JavaScript у браузер. **Q:** Як виміряти, чи SSR реально допомагає користувачам? **A:** Порівняйте First Contentful Paint (FCP) і Time to Interactive (TTI). SSR покращує FCP, бо користувачі бачать HTML одразу. TTI може не змінитись, бо React все одно потрібно завантажити і гідратувати. Вимірюйте через Web Vitals або real user monitoring інструменти. **Q (Senior):** Як реалізувати вибіркову гідратацію для сторінки, де інтерактивні тільки 10% компонентів? **A:** Використайте islands архітектуру. Рендерте статичні компоненти в HTML на сервері. Надсилайте JavaScript тільки для інтерактивних. Astro це реалізує нативно. В Next.js можна використати React Server Components, щоб тримати неінтерактивні компоненти поза клієнтським бандлом взагалі. Мета - зменшити JavaScript, що надсилається в браузер, а не просто відкласти його виконання. Менший бандл означає швидшу гідратацію тих 10%, що її реально потребують. ## Приклади ### Базовий: renderToString і hydrateRoot ```tsx // server.tsx import { renderToString } from 'react-dom/server'; import App from './App'; const html = renderToString(<App />); // Повертає: <div data-reactroot=""><h1>Hello</h1><button>Click me</button></div> // Без обробників подій. Без стану. Просто HTML. // client.tsx import { hydrateRoot } from 'react-dom/client'; import App from './App'; hydrateRoot(document.getElementById('root'), <App />); // React зіставляє компоненти з існуючими DOM-елементами. // Прикріплює обробники. Кнопка тепер інтерактивна. ``` Браузер відображає HTML одразу при отриманні. `hydrateRoot` потім підключає інтерактивність, не чіпаючи структуру DOM. Перебудови не відбувається. ### Середній рівень: сторінка продукту в Next.js ```tsx // pages/products/[id].tsx export async function getServerSideProps({ params }) { const product = await fetchProduct(params.id); return { props: { product } }; } export default function ProductPage({ product }) { const [quantity, setQuantity] = useState(1); return ( <div> <h1>{product.name}</h1> <p>${product.price}</p> <button onClick={() => setQuantity(q => q + 1)}> Додати в кошик ({quantity}) </button> </div> ); } // Сервер рендерить: <button>Додати в кошик (1)</button> // Браузер отримує і відображає HTML одразу. // React гідратує: прикріплює onClick. // Користувач натискає: quantity оновлюється. ``` Назва і ціна продукту видні до завантаження React. Кнопка стає інтерактивною після гідратації. Все з одного компонента. ### Просунутий: Streaming SSR з Suspense межами ```tsx // Кожна Suspense межа стрімиться незалежно. // React надсилає HTML частинами у міру готовності даних. import { Suspense } from 'react'; export default function ProductPage({ productId }) { return ( <div> <ProductHeader productId={productId} /> <Suspense fallback={<p>Завантаження відгуків...</p>}> <Reviews productId={productId} /> {/* Стрімиться, коли повертається запит до БД */} </Suspense> <Suspense fallback={<p>Завантаження рекомендацій...</p>}> <Recommendations productId={productId} /> {/* Стрімиться незалежно - повільний запит не блокує швидкий */} </Suspense> </div> ); } // ProductHeader приходить першим - користувач бачить його одразу. // Reviews і Recommendations стрімляться і гідратуються окремо. // Повільний запит рекомендацій не блокує решту сторінки. ``` Цей патерн означає, що повільний запит для одного блоку не затримує взаємодію з іншим. Кожна `Suspense` межа повністю незалежна.Для рев’юераПримітка для модератора (необов’язково)Бачить лише модератор. Прискорює рев’ю.