Запропонувати правкуПокращити цю статтюДопрацюйте відповідь до «Як працюють серверні компоненти (rsc) у Next.js». Ваші зміни проходять модерацію перед публікацією.Потрібне підтвердженняКонтентЩо ви змінюєте🇺🇸EN🇺🇦UAПереглядЗаголовок (UA)Коротка відповідь (UA)**React Server Components (RSC)** - це компоненти, які виконуються виключно на сервері. Їхній код ніколи не потрапляє до JavaScript-бандлу браузера. ```tsx // Server Component: виконується на сервері, нуль клієнтського JS async function Page() { const data = await db.problem.findMany() // тільки сервер return <ul>{data.map(p => <li key={p.id}>{p.name}</li>)}</ul> } ``` **Ключове:** Server Components не надсилають JavaScript до браузера. Client Components (позначені `'use client'`) відповідають за стан та інтерактивність. У Next.js App Router (13+) всі компоненти серверні за замовчуванням.Показується над повною відповіддю для швидкого нагадування.Відповідь (UA)Зображення**React Server Components (RSC)** - це компоненти, які виконуються лише на сервері і не надсилають жодного JavaScript у браузер для серверних частин інтерфейсу. ## Теорія ### TL;DR - Server Components виконуються один раз на запит, на сервері. Їхній код ніколи не потрапляє до клієнтського бандлу. - Аналогія: кухня, яка готує страву і подає її через ліфт. Кухарі й рецепти не приходять до твого столу. - Результат надходить до браузера як RSC payload - бінарний стрім, а не JavaScript. - Головне правило вибору: отримуєш дані, маєш секрети, використовуєш важкі бібліотеки? Server Component. Потрібен стан, обробники подій, браузерне API? Client Component з `'use client'`. - У Next.js App Router (13+) всі компоненти за замовчуванням є серверними. ### Короткий приклад ```tsx // app/page.tsx - Server Component (без 'use client' = сервер за замовчуванням) import { db } from '@/lib/db' export default async function ProblemsPage() { // Виконується на сервері. Імпорт db ніколи не потрапляє до браузера. const problems = await db.problem.findMany() return <ul>{problems.map(p => <li key={p.id}>{p.name}</li>)}</ul> } ``` ```tsx // app/components/FilterButton.tsx - Client Component 'use client' import { useState } from 'react' export function FilterButton({ onFilter }) { const [active, setActive] = useState(false) return ( <button onClick={() => { setActive(!active); onFilter(active) }}> Фільтр </button> ) } ``` Серверний компонент отримує дані і рендерить HTML. Клієнтський обробляє кліки. Без перетину. ### Головна різниця від SSR SSR (Server-Side Rendering) рендерить все дерево React на сервері один раз і надсилає повний JavaScript-бандл для гідратації. RSC - інша модель: серверні частини дерева назавжди залишаються на сервері, не надсилають код клієнту і перерендерюються на сервері при навігації. Бібліотека для графіків на 200 КБ залишається на сервері. Браузер отримує тільки результат. ### Правила вибору - **Отримати дані з БД або файлової системи** - Server Component. Прямий `await db.query...` без API-прошарку. - **Доступ до змінних середовища або секретів** - Server Component. `process.env.DB_URL` ніколи не виточе. - **Важкі залежності (обробка зображень, парсер Markdown)** - Server Component. Зменшує клієнтський бандл. - **Стан, хуки або обробники подій** - Client Component. Додаєш `'use client'` на початку файлу. - **Браузерне API (localStorage, геолокація)** - тільки Client Component. - **Інтерактивні форми, real-time UI** - Client Component. ### Таблиця порівняння | Аспект | Server Components (за замовчуванням) | Client Components (`'use client'`) | |---|---|---| | Виконання | Node.js, один раз на запит | Браузер, після завантаження JS бандлу | | Вплив на бандл | Нульовий - тільки RSC payload | Повний код компонента + залежності | | Доступ до даних | Прямо: БД, файли, змінні середовища | Через props від сервера або fetch до API | | Хуки / обробники подій | Недоступні | Повна підтримка | | Повторний рендер на клієнті | Ніколи | При зміні стану або props | | Де застосовувати | Дашборди з даними, списки | Форми, модалки, інтерактивні острівці | ### Як RSC працює під капотом Next.js виконує Server Components у Node.js під час обробки запиту. Результат серіалізується у **RSC payload**: бінарний стрім із інструкціями для React runtime у браузері. Payload містить відрендерений HTML для серверних частин і маркери-"дірки" (holes) там, де мають бути гідратовані Client Components. Браузер отримує HTML і RSC payload, одразу вставляє статичний контент, а потім гідратує тільки клієнтські острівці. При навігації на новий маршрут Next.js запитує RSC payload для цього маршруту. Server Components перерендерюються на сервері. Client Components зберігають свій стан. Код серверних компонентів браузер не завантажує ніколи. На практиці я помітив: команди, що переходять з Pages Router, за звичкою додають `'use client'` скрізь. Результат - клієнтський бандл, ідентичний тому, що був до міграції. За замовчуванням все серверне, і ти переходиш на клієнт тільки тоді, коли справді потрібна інтерактивність. ### Поширені помилки **1. Використання `useState` в Server Component** ```tsx // Неправильно - помилка збірки в Next.js 13+ export default function Page() { const [count, setCount] = useState(0) // Хуки не існують на сервері return <div>{count}</div> } ``` Вирішення: перенеси стан у дочірній Client Component, передай початкові дані через props. **2. Отримання чутливих даних у Client Component** ```tsx // Неправильно - ключ API видно у бандлі браузера 'use client' export function Users() { useEffect(() => { fetch(`/api/users?key=${process.env.NEXT_PUBLIC_SECRET}`) // витік }, []) } ``` Отримуй дані у Server Component і передавай результат через props. Секрети залишаються на сервері. **3. `'use client'` скрізь** Позначення компонента як `'use client'` затягує всі його імпорти до клієнтського бандлу. Бібліотека для графіків на 200 КБ стає 200 КБ клієнтського JavaScript. За замовчуванням - Server. `'use client'` тільки для найменшого інтерактивного листка. **4. Спроба імпортувати Server Component у Client Component** ```tsx // Неправильно - клієнтські файли не можуть імпортувати серверні модулі 'use client' import { ServerCard } from './server-card' // порушує серверний кордон ``` Передавай відрендерений сервером контент через `children` або `props`. ### Де зустрічається на практиці - Vercel dashboard: Server Components отримують таблиці даних через Prisma, Client Components обробляють фільтри. - T3 Stack (create-t3-app): серверні сторінки отримують дані з Clerk або tRPC, клієнтські обробляють оптимістичні оновлення. - Shadcn/UI + Next.js 14: сервер рендерить `<Table>` з даними БД, клієнт керує станом `<Dialog>`. - Будь-який платіжний дашборд: списки транзакцій тільки на сервері, Stripe SDK не потрапляє до браузера. ### Питання на співбесіді **Q:** Що таке RSC payload? **A:** Бінарний стрім, який Next.js надсилає разом з HTML. Містить відрендерений результат Server Components і маркери-"дірки" для клієнтських меж. React runtime у браузері читає його для оновлення DOM без повторного завантаження серверного коду. **Q:** Чим RSC відрізняється від SSR? **A:** SSR надсилає HTML плюс повний JavaScript-бандл для гідратації всього дерева на клієнті. RSC стрімить частковий payload без JS для серверних частин. Server Components ніколи не гідратуються. Гідратуються тільки Client Components. **Q:** Чи можуть Server Components використовувати React context? **A:** Ні. Context потребує клієнтського runtime. Дані між Server Components передаються через props або отримуються повторно в кожному компоненті (Next.js автоматично дедуплікує однакові `fetch`-запити). **Q:** Як працює кешування в Server Components? **A:** Next.js кешує `fetch`-запити за замовчуванням. Використовуй `cache: 'force-cache'` для статичних даних або `unstable_cache` для не-fetch-джерел, наприклад прямих запитів до БД. **Q:** (Senior) У вкладеному дереві Suspense є повільна внутрішня межа і швидка зовнішня. Як RSC це обробляє? **A:** Next.js стрімить RSC payload частинами. Швидка зовнішня межа вирішується першою і надходить до браузера, який рендерить fallback для повільної внутрішньої. Коли повільна межа вирішується на сервері, Next.js стрімить chunk із заміною. React runtime у браузері замінює fallback без повного перерендеру сторінки. Жодного клієнтського JavaScript для серверних частин. ## Приклади ### Базовий: сервер отримує дані, клієнт фільтрує ```tsx // app/problems/page.tsx (Server Component) import { db } from '@/lib/db' import { ProblemList } from './problem-list' export default async function ProblemsPage() { const problems = await db.problem.findMany({ orderBy: { difficulty: 'asc' } }) // Передаємо серіалізовані дані до клієнтського компонента return <ProblemList problems={problems} /> } ``` ```tsx // app/problems/problem-list.tsx (Client Component) 'use client' import { useState } from 'react' export function ProblemList({ problems }) { const [filter, setFilter] = useState('all') const filtered = filter === 'all' ? problems : problems.filter(p => p.difficulty === Number(filter)) return ( <div> <select onChange={e => setFilter(e.target.value)}> <option value="all">Усі</option> <option value="1">Легкі</option> <option value="2">Середні</option> <option value="3">Важкі</option> </select> <ul>{filtered.map(p => <li key={p.id}>{p.name}</li>)}</ul> </div> ) } ``` Сервер отримує всі задачі один раз. Фільтрація виконується у браузері на вже отриманих даних. Жодного додаткового API-запиту при зміні фільтра. ### Середній: Server Component як children Client Components не можуть імпортувати Server Components. Але можуть отримувати їх через `children`. Це дозволяє будувати інтерактивні обгортки, які всередині зберігають переваги серверного рендерингу. ```tsx // components/accordion.tsx (Client Component - керує відкриттям/закриттям) 'use client' import { useState } from 'react' export function Accordion({ title, children }) { const [open, setOpen] = useState(false) return ( <div> <button onClick={() => setOpen(!open)}>{title}</button> {open && children} </div> ) } ``` ```tsx // app/faq/page.tsx (Server Component - отримує дані, використовує клієнтську обгортку) import { Accordion } from '@/components/accordion' import { db } from '@/lib/db' export default async function FAQ() { const items = await db.faq.findMany() return ( <div> {items.map(item => ( <Accordion key={item.id} title={item.question}> <p>{item.answer}</p> {/* Відрендерений сервером контент */} </Accordion> ))} </div> ) } ``` `<p>{item.answer}</p>` рендерить Server Component. Accordion керує станом перемикача на клієнті. Дані FAQ ніколи не потрапляють до клієнтського бандлу. ### Просунутий: стрімінг з Suspense Коли одна частина сторінки повільна (складний запит до БД на 2 секунди), можна стрімити швидкі частини одразу, а повільну - коли вона буде готова. ```tsx // app/dashboard/page.tsx import { Suspense } from 'react' import { TeamStats } from './team-stats' // повільно - складна агрегація в БД import { QuickLinks } from './quick-links' // швидко - статичні або кешовані дані export default function DashboardPage() { return ( <div> <Suspense fallback={<p>Завантаження статистики...</p>}> <TeamStats /> {/* Стрімиться коли готово */} </Suspense> <QuickLinks /> {/* Стрімиться одразу */} </div> ) } ``` ```tsx // app/dashboard/team-stats.tsx (Server Component) import { db } from '@/lib/db' export async function TeamStats() { const stats = await db.stats.aggregate({ _sum: { score: true } }) return <div>Загальний рахунок команди: {stats._sum.score}</div> } ``` `QuickLinks` з'являється одразу. `TeamStats` показує "Завантаження..." до завершення запиту, потім сервер стрімить реальний контент. Жодного JavaScript в браузері для жодного з компонентів.Для рев’юераПримітка для модератора (необов’язково)Бачить лише модератор. Прискорює рев’ю.