Запропонувати правкуПокращити цю статтюДопрацюйте відповідь до «Як працює рендеринг на стороні клієнта (CSR) у Next.js». Ваші зміни проходять модерацію перед публікацією.Потрібне підтвердженняКонтентЩо ви змінюєте🇺🇸EN🇺🇦UAПереглядЗаголовок (UA)Коротка відповідь (UA)**CSR (рендеринг на стороні клієнта) у Next.js** означає, що клієнтські компоненти з директивою `'use client'` гідратуються і виконуються в браузері поверх HTML, згенерованого сервером. На відміну від чистого SPA, сервер завжди рендерить першим. ```tsx 'use client' import { useState } from 'react' export default function Counter() { const [n, setN] = useState(0) return <button onClick={() => setN(n + 1)}>{n}</button> } ``` **Ключове:** CSR у Next.js завжди накладається поверх серверного HTML. Використовуй для інтерактивності та browser API, не для початкового завантаження даних.Показується над повною відповіддю для швидкого нагадування.Відповідь (UA)Зображення**CSR (рендеринг на стороні клієнта, client-side rendering) у Next.js** означає, що клієнтський компонент з директивою `'use client'` виконується і оновлюється в браузері після того, як сервер надіслав початковий HTML від серверного компонента. ## Теорія ### TL;DR - Next.js ніколи не робить чистий CSR, як Create React App. Кожна сторінка починається з серверного компонента, що віддає реальний HTML, а не порожній `<div>`. - Клієнтські компоненти (`'use client'`) гідратуються в браузері й управляють станом, подіями та browser API. - `hydrateRoot` з React 18 підключається до існуючих DOM-вузлів без повного перерендерингу. - Використовуй CSR для дій користувача, browser API та real-time UI. Дані для першого завантаження краще отримувати в серверних компонентах. - Кожен `'use client'` модуль збільшує клієнтський JS-бандл. Тримай межу якомога глибше в дереві. ### Швидкий приклад ```tsx // app/dashboard/page.tsx - Server Component import ClientDashboard from './ClientDashboard' export default function DashboardPage() { return ( <div> <h1>Dashboard</h1> <ClientDashboard /> {/* гідратується в браузері */} </div> ) } // app/dashboard/ClientDashboard.tsx 'use client' import { useState, useEffect } from 'react' export default function ClientDashboard() { const [data, setData] = useState<{ name: string } | null>(null) useEffect(() => { fetch('/api/user-data') .then(res => res.json()) .then(setData) }, []) return <div>{data ? `Користувач: ${data.name}` : 'Завантаження...'}</div> } ``` Сервер надсилає `<h1>Dashboard</h1><div>Завантаження...</div>`. Браузер отримує це, `hydrateRoot` підключає React до існуючого DOM, запускається `useEffect`, приходять дані, і компонент показує "Користувач: John". Два етапи, одна сторінка. ### Чим CSR у Next.js відрізняється від чистого SPA Create React App відправляє порожній `<div id="root"></div>` і повний JS-бандл. Браузер повинен завантажити, розібрати й виконати все це до того, як покаже хоча б один символ. SEO-боти нічого не бачать. TTFB швидкий, але FCP (First Contentful Paint) повільний. Next.js так не робить. Навіть якщо компонент позначений `'use client'`, сторінка все одно починається з серверного компонента, що генерує реальний HTML. Клієнтський компонент також рендериться на сервері для початкового знімка. Браузер отримує цей знімок як HTML, `hydrateRoot` підключається до існуючих DOM-вузлів, і компонент стає інтерактивним. Ніякого повного перерендерингу. Ніякого порожнього екрану. Головна ідея: у Next.js CSR завжди накладається поверх серверного HTML. ### Коли використовувати - Завантаження даних за діями користувача (пошук, фільтри, пагінація): CSR з `useEffect` і `fetch`, або TanStack Query для кешування. - Browser API (`localStorage`, `navigator.geolocation`, `WebSocket`, canvas): їх немає в Node.js, тому компонент має бути клієнтським. - Real-time widgets (чат, live-графіки, сповіщення): клієнтські компоненти з WebSocket або polling. - SEO-контент і статичні дані: залишай у серверних компонентах. Жодного JS не відправляється в браузер. - Продуктивність на мобільних: мінімізуй кількість `'use client'` меж. Кожна збільшує бандл. ### Як гідратація (hydration) працює всередині Браузер отримує HTML-рядок і JS-бандл від Next.js сервера (Node.js або Edge Runtime). React викликає `hydrateRoot(container, element)`, проходить по існуючому DOM і прикріплює внутрішнє fiber-дерево до вузлів без їх модифікації. Потім запускаються всі `useEffect`. V8 виконує тільки ті модулі, що доступні через межі `'use client'`. Код серверних компонентів до браузера не потрапляє. З React Server Components і `<Suspense>` браузер може починати гідратувати частини сторінки по мірі їх надходження, не чекаючи повної відповіді. Клієнтський компонент всередині Suspense-межі може стати інтерактивним ще до того, як сервер завершив надсилання рештки сторінки. Якщо server-rendered HTML не збігається з тим, що React очікує побачити в браузері, виникає hydration mismatch. React перерендерить цей піддерево з нуля, що скасовує перевагу SSR для цього компонента. ### Типові помилки **Позначати цілу сторінку як клієнтський компонент без причини.** ```tsx // Неправильно: вся сторінка стає CSR, без переваг SSR 'use client' export default function Page() { const [data, setData] = useState(null) useEffect(() => { fetch('/api/data').then(r => r.json()).then(setData) }, []) return <div>{data?.name}</div> } // Правильно: серверний компонент отримує дані, JS не потрібен export default async function Page() { const data = await fetch('/api/data').then(r => r.json()) return <div>{data.name}</div> } ``` Неправильний варіант відправляє порожній HTML на перший запит, шкодить SEO і завантажує зайвий JS у браузер. **Використовувати browser globals поза `useEffect`.** ```tsx // Неправильно: крешується під час серверного рендерингу const [width, setWidth] = useState(window.innerWidth) // window немає в Node.js // Правильно: безпечне початкове значення, оновлення після монтування const [width, setWidth] = useState(0) useEffect(() => setWidth(window.innerWidth), []) ``` **Hydration mismatch через недетерміновані значення.** Якщо початковий рендер залежить від `Date.now()`, `Math.random()` або стану браузера, сервер і браузер генерують різний HTML. React попереджає і перерендерить. Завжди давай browser-залежному стану безпечне початкове значення і встановлюй реальне в `useEffect`. **Відсутність обробки помилок у CSR-запитах.** `fetch` у `useEffect` без `try/catch` тихо ламає компонент. Користувач бачить порожню секцію. Додай try/catch і покажи стан помилки, або використай SWR, який обробляє це за замовчуванням. **`'use client'` занадто високо в дереві.** Бачив команди, що додають `'use client'` на рівні layout за замовчуванням. Це означає, що весь піддерево відправляється як клієнтський JS. Перенесення межі вниз до реального інтерактивного leaf-компонента скоротило розмір бандлу вдвічі на одному проекті, де я з цим зіткнувся. ### Де використовується - Vercel Dashboard: CSR для живих графіків і фільтрів через `useEffect` і fetch. - Linear: клієнтські компоненти з WebSocket для real-time редагування. - Clerk: `'use client'` для модалок авторизації та роботи з `localStorage`. - Recharts у Next.js: завжди CSR, бо бібліотека потребує `window` і canvas. - Будь-який пошук у реальному часі: `useEffect` + `AbortController` для скасування застарілих запитів. ### Питання від інтерв'юерів **Q:** Чим гідратація Next.js відрізняється від Create React App? **A:** CRA відправляє порожній div, і браузер рендерить все з JS. Next.js відправляє готовий HTML від серверних компонентів, тому контент видно одразу. `hydrateRoot` підключає React до існуючого DOM без повного перерендерингу. **Q:** Що викликає hydration mismatch? **A:** Різний HTML на сервері і в браузері для одного компонента. Типові причини: `Date.now()`, `Math.random()`, читання `window` або `localStorage` при початковому рендерингу. Рішення: переноси browser-логіку в `useEffect`. **Q:** `useEffect` + `fetch` або SWR для CSR-даних? **A:** `useEffect` для разових запитів без потреби в кешуванні. SWR або TanStack Query для stale-while-revalidate, дедуплікації і refetch при фокусі. Більшість production-проектів в результаті переходять на SWR. **Q:** Чи поширюється `'use client'` на всі імпорти всередині файлу? **A:** Так. Кожен модуль, що імпортується клієнтським компонентом, також виконується в браузері. Тому межу тримають якомога глибше: `'use client'` отримують тільки компоненти, яким справді потрібні стан або події. **Q:** Як partial prerendering у App Router v14+ взаємодіє з CSR? **A:** Статична оболонка збирається на сервері під час деплою. Клієнтські компоненти всередині Suspense-меж пробивають дірки в цій оболонці та гідратуються самостійно. Динамічні слоти не блокують статичний контент, тому отримуємо миттєвий статичний HTML плюс ізольовану CSR-гідратацію без повної ревалідації. ## Приклади ### Базовий: пошуковий інпут з клієнтським запитом ```tsx // app/search/SearchBox.tsx 'use client' import { useState } from 'react' interface Result { id: string; title: string } export default function SearchBox() { const [query, setQuery] = useState('') const [results, setResults] = useState<Result[]>([]) const search = async () => { const res = await fetch(`/api/search?q=${encodeURIComponent(query)}`) setResults(await res.json()) } return ( <div> <input value={query} onChange={e => setQuery(e.target.value)} placeholder="Пошук..." /> <button onClick={search}>Знайти</button> <ul>{results.map(r => <li key={r.id}>{r.title}</li>)}</ul> </div> ) } ``` Сервер відправляє статичну сторінку. Пошукове поле гідратується в браузері і отримує результати за запитом. Ніяких перезавантажень сторінки. ### Середній: серверні дані передаються в клієнтський фільтр ```tsx // app/problems/page.tsx - Server Component import { db } from '@/lib/db' import { ProblemFilter } from './ProblemFilter' export default async function ProblemsPage() { const problems = await db.problem.findMany({ select: { id: true, name: true, difficulty: true } }) return ( <div> <h1>Задачі</h1> <ProblemFilter initialProblems={problems} /> </div> ) } // app/problems/ProblemFilter.tsx - Client Component 'use client' import { useState } from 'react' interface Problem { id: string; name: string; difficulty: number } export function ProblemFilter({ initialProblems }: { initialProblems: Problem[] }) { const [difficulty, setDifficulty] = useState<number | null>(null) const filtered = difficulty ? initialProblems.filter(p => p.difficulty === difficulty) : initialProblems return ( <div> <button onClick={() => setDifficulty(null)}>Всі</button> <button onClick={() => setDifficulty(1)}>Легкі</button> <button onClick={() => setDifficulty(2)}>Середні</button> <button onClick={() => setDifficulty(3)}>Важкі</button> <ul>{filtered.map(p => <li key={p.id}>{p.name}</li>)}</ul> </div> ) } ``` Сервер завантажує дані один раз і передає їх як props. Фільтрація відбувається в пам'яті на клієнті. Ніяких API-запитів при кожному натисканні кнопки. ### Просунутий: dynamic import з вимкненим SSR для browser-only компонента Деякі компоненти використовують API, яких немає на сервері. Редактор коду на базі Monaco, canvas-рендерер або WebSocket-клієнт упадуть при серверному рендерингу, якщо їх імпортувати звичайним способом. ```tsx // app/problem/[id]/page.tsx - Server Component import dynamic from 'next/dynamic' // ssr: false - цей компонент ніколи не запуститься в Node.js const CodeEditor = dynamic( () => import('@/components/CodeEditor'), { ssr: false, loading: () => <p>Завантаження редактора...</p> } ) export default function ProblemPage({ params }: { params: { id: string } }) { return ( <div> <h1>Задача {params.id}</h1> <CodeEditor /> {/* тільки в браузері */} </div> ) } // components/CodeEditor.tsx 'use client' import { useEffect, useRef } from 'react' export default function CodeEditor() { const ref = useRef<HTMLDivElement>(null) useEffect(() => { // Monaco потребує window і canvas - тут це безпечно, ми в браузері if (ref.current) { // ініціалізація редактора } }, []) return <div ref={ref} style={{ height: 400 }} /> } ``` `ssr: false` гарантує, що компонент ніколи не запускається в Node.js. Сторінка все одно відправляє серверний HTML з placeholder-ом, який видно одразу. Редактор завантажується після гідратації.Для рев’юераПримітка для модератора (необов’язково)Бачить лише модератор. Прискорює рев’ю.