Запропонувати правкуПокращити цю статтюДопрацюйте відповідь до «Компоненти сервера React (rsc)». Ваші зміни проходять модерацію перед публікацією.Потрібне підтвердженняКонтентЩо ви змінюєте🇺🇸EN🇺🇦UAПереглядЗаголовок (UA)Коротка відповідь (UA)**React Server Components (RSC)** - це компоненти, які виконуються тільки на сервері під час кожного запиту і надсилають браузеру HTML без власного JavaScript. Вони можуть звертатись до Node.js-ресурсів: баз даних, файлової системи і т.д. - напряму. ```tsx async function UserProfile({ userId }: { userId: string }) { const user = await db.user.findUnique({ where: { id: userId } }); return <div><h1>{user.name}</h1><p>{user.email}</p></div>; } ``` **Ключове:** RSC не додають нічого до JS-бандлу. `"use client"` потрібен тільки тоді, коли є стан, обробники подій або браузерні API.Показується над повною відповіддю для швидкого нагадування.Відповідь (UA)Зображення**React Server Components (RSC)** рендеряться на сервері під час кожного запиту і надсилають браузеру HTML або посилання на клієнтські компоненти, не відправляючи власний JavaScript. ## Теорія ### TL;DR - RSC як кухня ресторану: ти отримуєш готову страву (HTML) за столом (у браузері), кухонне приладдя (JS-код) до тебе не їде - Головна різниця: RSC мають нульовий JS-бандл; клієнтські компоненти відправляють JS для інтерактивності - RSC може рендерити клієнтські компоненти; клієнтський компонент не може рендерити серверні (але може приймати їх через `children`) - За замовчуванням у Next.js App Router - директива не потрібна; `"use client"` додаєш тільки для інтерактивності ### Базовий приклад ```tsx // app/user/[id]/page.tsx - Серверний компонент (за замовчуванням у Next.js) async function UserPage({ params }: { params: { id: string } }) { // Прямий доступ до БД - API маршрут не потрібен const user = await db.user.findUnique({ where: { id: params.id } }); return ( <div> <h1>{user.name}</h1> <p>{user.email}</p> <LikeButton userId={params.id} /> {/* Клієнтський компонент - відправляється як JS */} </div> ); } // JS для UserPage ніколи не потрапляє до браузера. JS для LikeButton - так. ``` `UserPage` виконується один раз на сервері для кожного запиту. HTML для `h1` і `p` стрімиться одразу. `LikeButton` потрапляє до JS-бандлу, бо йому потрібен `useState`. ### Головна різниця RSC виконуються в Node.js - вони можуть викликати Prisma, читати з `fs`, використовувати `cookies()` та `headers()` з `next/headers`. Клієнтські компоненти працюють у браузері (і під час SSR), тому їм доступні Web API: `window`, `document` і т.д. Практичний результат: статичні частини інтерфейсу не відправляють жодного JavaScript. У продакшн Next.js-додатках це скорочує JS-бандл на 90% і більше для сторінок, де переважає відображення даних. ### Серверний vs клієнтський: що кожен може | Особливість | Серверний компонент | Клієнтський компонент | |---|---|---| | Директива `"use client"` | Ні | Так | | Виконується на | Тільки сервері | Сервер (SSR) + браузер | | Внесок у JS-бандл | Нуль | Включено | | `useState`, `useEffect` | Ні | Так | | Обробники подій (`onClick`) | Ні | Так | | `async` компонент | Так | Ні | | Прямий доступ до БД / файлової системи | Так | Ні - тільки через API | | Браузерні API (`window`, `document`) | Ні | Так | ### Правила композиції (composition) ``` Серверний компонент → може рендерити серверні або клієнтські компоненти ✅ Клієнтський компонент → може рендерити тільки клієнтські компоненти ❌ Клієнтський компонент → може приймати серверний компонент через children prop ✅ ``` Патерн `children` - це виходна точка, коли потрібна клієнтська оболонка навколо серверного вмісту: ```tsx // layout.tsx - Серверний компонент передає children клієнтському export default function Layout({ children }: { children: React.ReactNode }) { return ( <ClientSidebar> {children} {/* Тут може бути ціле дерево серверних компонентів */} </ClientSidebar> ); } ``` React обчислює `children` на сервері перед тим, як передати результат у `ClientSidebar`. Клієнтський компонент отримує вже відрендерений результат, а не сам серверний компонент. ### Як це працює всередині Next.js використовує React Server Component Runtime у Node.js. Під час запиту він один раз обходить дерево компонентів і серіалізує вивід RSC у бінарний RSC Payload через `react-server-dom-webpack/server`. Цей payload містить готовий HTML для серверних компонентів і посилання на модулі для клієнтських. Браузер отримує стрім, рендерить слоти клієнтських компонентів з відповідним JS і пропускає повторне виконання RSC-коду повністю. Гідрація для серверних компонентів не відбувається. Це принципово відрізняється від класичного SSR, де кожен компонент повторно виконується в браузері для підключення обробників подій. ### Коли що використовувати - Завантаження даних для сторінки продукту, профілю, дашборду: RSC (прямий доступ до БД, нуль JS) - Інтерактивні кнопки, форми, модальні вікна: клієнтський компонент, вкладений у RSC - Секрети - API-ключі, облікові дані БД: RSC (до браузера не потрапляє) - Рендеринг Markdown, підсвічування синтаксису, важкі бібліотеки: RSC (залишаються на сервері) - WebSocket, canvas, localStorage: клієнтський компонент (тільки браузерні API) ### Стрімінг із Suspense ```tsx // app/dashboard/page.tsx import { Suspense } from 'react'; async function SlowAnalytics() { const data = await fetchAnalytics(); // 2 секунди return <Chart data={data} />; } async function RecentUsers() { const users = await db.user.findMany({ take: 5 }); return <ul>{users.map(u => <li key={u.id}>{u.name}</li>)}</ul>; } export default function Dashboard() { return ( <div> <h1>Дашборд</h1> <Suspense fallback={<p>Завантаження аналітики...</p>}> <SlowAnalytics /> </Suspense> <Suspense fallback={<p>Завантаження користувачів...</p>}> <RecentUsers /> </Suspense> </div> ); } // Оболонка HTML стрімиться миттєво. Обидва компоненти завантажуються паралельно. ``` Кожна межа `Suspense` незалежна. Браузер рендерить оболонку одразу, потім заповнює секції у міру надходження даних. Це напряму впливає на Time to Interactive: сторінка вже придатна до роботи до завантаження всіх даних. ### Типові помилки **Завантаження даних у клієнтському компоненті, коли RSC вже має їх:** ```tsx // Неправильно - подвійний запит, відкриває API "use client"; async function Likes({ userId }) { const count = await api.likes(userId); return <span>{count}</span>; } // Правильно - завантаження в RSC, передача через prop async function LikeSection({ userId }) { // Серверний компонент const count = await db.likes.count({ where: { userId } }); return <LikeDisplay count={count} />; // Клієнтський компонент отримує дані, а не робить fetch } ``` **Імпорт браузерних модулів у RSC:** ```tsx // Неправильно - помилка під час збірки import { useEffect } from 'react'; // У серверному компоненті import { Chart } from 'chart.js'; // Тільки для браузера ``` Перенеси в файл із `"use client"` або використай `dynamic(() => import(...), { ssr: false })`. **Директива `"use client"` занадто високо в дереві:** ```tsx // Неправильно - тягне все піддерево в бандл "use client"; export default function Page() { // Мав бути серверним компонентом return <div><ProductList /></div>; // ProductList теж стає клієнтським } ``` Тримай `"use client"` на листках - тільки на інтерактивних частинах. Команда Vercel зафіксувала 70% регресію продуктивності через необдуманий переклад батьківських компонентів у клієнтські. **Спроба використати стан у RSC:** ```tsx // Неправильно - RSC не мають стану, рендеряться один раз на запит function Counter() { const [count, setCount] = useState(0); // Помилка: хуки не підтримуються в серверних компонентах return <p>{count}</p>; } ``` ### Де використовується в реальних проектах - Next.js 14+ App Router: сторінки та layouts за замовчуванням є RSC. Vercel Commerce використовує RSC для 100+ сторінок лістингу з нульовим JS для самого лістингу. - PayloadCMS і Waku використовують RSC для адмін-дашбордів із прямим доступом через Prisma. - React 19 підтримує RSC поза Next.js через `react-server-dom-webpack` в Express. - Патерн "RSC для даних, клієнтський компонент для взаємодії" точно відповідає структурі більшості сучасних продакшн-додатків. ### Питання на співбесіді **Q:** Чи може RSC читати cookies або заголовки автентифікації? **A:** Так. Використовуй `cookies()` і `headers()` з `next/headers`. Вони доступні тільки на сервері і недоступні в клієнтських компонентах. **Q:** У якому форматі передається RSC Payload? **A:** Це бінарний стрім із посиланнями на модулі, щось на кшталт `[ref:0,"div",[ref:1,"h1","John Doe"]]`. Клієнт розшифровує їх через webpack module map для рендерингу слотів клієнтських компонентів. **Q:** Як тестувати серверний компонент? **A:** Через `react-test-renderer` для отримання статичного дерева. `act()` не потрібен, бо RSC не мають стану. Для інтеграційних тестів підходить `@testing-library/react` з замоканим async-компонентом. **Q:** Що змінює React 19 порівняно з RSC у Next.js? **A:** React 19 додає примітив `use` для клієнтських компонентів - вони можуть читати серверні дані з Promise. Next.js 15 додав Turbopack, який прискорює збірку RSC приблизно в 10 разів. Сама модель RSC не змінилась. **Q:** Поясни оптимізацію flight graph та підйом (hoisting) дерева. **A:** Клієнтські компоненти піднімаються вгору дерева і дедублюються в бандлі. Flight graph (серіалізований RSC payload) об'єднує дублікати піддерев на сервері, тому повторювані компоненти на кшталт навігації або футера не відправляються кілька разів. Це відбувається всередині `@react-server-dom-webpack` під час серіалізації. ## Приклади ### Базовий: серверний компонент із прямим доступом до БД ```tsx // app/products/[id]/page.tsx import { db } from '@/db'; import { eq } from 'drizzle-orm'; import { products } from '@/db/schema'; import AddToCart from '@/components/AddToCart'; // "use client" async function ProductPage({ params }: { params: { id: string } }) { const product = await db.query.products.findFirst({ where: eq(products.id, params.id), with: { variants: true } }); return ( <article> <h1>{product.name}</h1> <p>${product.price}</p> <AddToCart productId={product.id} /> {/* Тільки цей компонент відправляє JS */} </article> ); } ``` `ProductPage` не відправляє жодного JavaScript. Запит до БД виконується на сервері. `AddToCart` - єдиний компонент у JS-бандлі, бо йому потрібен `onClick`. ### Середній: стрімінг дашборду з паралельними запитами ```tsx // app/dashboard/page.tsx import { Suspense } from 'react'; async function Analytics() { const data = await fetchAnalyticsReport(); // Повільний зовнішній API return <BarChart data={data} />; } async function RecentOrders() { const orders = await db.select().from(ordersTable).limit(10); return ( <ul> {orders.map(o => ( <li key={o.id}>{o.product} - ${o.total}</li> ))} </ul> ); } export default function Dashboard() { return ( <> <h1>Дашборд</h1> <Suspense fallback={<div>Завантаження аналітики...</div>}> <Analytics /> </Suspense> <Suspense fallback={<div>Завантаження замовлень...</div>}> <RecentOrders /> </Suspense> </> ); } ``` `Analytics` і `RecentOrders` завантажуються паралельно. Оболонка сторінки рендериться миттєво. Кожна секція з'являється, коли її дані готові. Жодний JavaScript не чекає на жоден із запитів на клієнті. ### Просунутий: важкі бібліотеки залишаються на сервері ```tsx // app/blog/[slug]/page.tsx import { marked } from 'marked'; // ~50KB - залишається на сервері import hljs from 'highlight.js'; // ~100KB - залишається на сервері import ShareButton from '@/components/ShareButton'; // "use client" - потрібен для clipboard API async function BlogPost({ params }: { params: { slug: string } }) { const post = await getPostBySlug(params.slug); // Обидві бібліотеки виконуються на сервері і ніколи не потрапляють до браузера const html = marked(post.content); return ( <article> <h1>{post.title}</h1> <div dangerouslySetInnerHTML={{ __html: html }} /> <ShareButton url={`/blog/${params.slug}`} /> {/* ~2KB JS */} </article> ); } ``` `marked` і `highlight.js` разом важать близько 150KB. У CSR-додатку обидві бібліотеки відправляються кожному відвідувачу. Тут вони виконуються один раз на сервері для кожного запиту. Клієнт отримує HTML і ~2KB JS для `ShareButton`. Це весь бандл для цієї сторінки.Для рев’юераПримітка для модератора (необов’язково)Бачить лише модератор. Прискорює рев’ю.