Что такое Server Components в React?
Server Components в React
Server Components (RSC) — архитектурная возможность React, стабилизированная в React 19, позволяющая выполнять рендеринг компонентов целиком на сервере. Результат рендеринга — не HTML-строка (как в SSR), а специальный сериализуемый формат React-дерева, который стримится на клиент и встраивается в существующее дерево без перезагрузки страницы.
Ключевые отличия от обычных компонентов
- Нет клиентского JS: код Server Component не включается в бандл браузера — значит, тяжёлые библиотеки (парсеры, ORM, SDK) остаются только на сервере.
- Прямой доступ к серверным ресурсам: можно делать
async/awaitзапросы к БД, читать файлы, обращаться к внутренним API прямо внутри компонента. - Нет состояния и эффектов:
useState,useEffect, обработчики событий — недоступны. Для интерактивности нужны Client Components. - Стриминг: RSC поддерживают стриминг через
<Suspense>, что позволяет показывать части UI сразу, не дожидаясь всех данных.
Разграничение Server / Client
По умолчанию в Next.js App Router все компоненты — серверные. Чтобы сделать компонент клиентским, нужна директива 'use client' в начале файла.
// ServerComponent — рендерится только на сервере
async function UserProfile({ userId }: { userId: string }) {
// Прямой запрос к БД — безопасно, не попадёт в браузер
const user = await db.user.findUnique({ where: { id: userId } });
return (
<div>
<h2>{user.name}</h2>
{/* Клиентский компонент встроен внутрь серверного */}
<LikeButton postId={userId} />
</div>
);
}
Граница Server/Client и паттерн «дырки»
Client Component не может импортировать Server Component напрямую, но Server Component может передавать другие Server Components как children в Client Component — это называется паттерном «дырок» (slots / children composition).
// ClientWrapper.tsx
'use client';
export function ClientWrapper({ children }: { children: React.ReactNode }) {
const [open, setOpen] = useState(false);
return <div onClick={() => setOpen(!open)}>{children}</div>;
}
// Page.tsx — серверный файл
import { ClientWrapper } from './ClientWrapper';
import { ServerData } from './ServerData'; // серверный компонент
export default function Page() {
return (
<ClientWrapper>
<ServerData /> {/* допустимо — передаём как children */}
</ClientWrapper>
);
}
Сериализуемые пропсы
Пропсы, передаваемые из Server Component в Client Component, должны быть сериализуемыми (JSON-совместимыми). Функции, классы, Set, Map — не сериализуются и вызовут ошибку.
Преимущества для производительности
- Меньше бандл — тяжёлые зависимости остаются на сервере.
- Нет водопада клиентских запросов — данные фетчатся параллельно на сервере.
- Автоматическое кэширование — Next.js добавляет кэш на уровне
fetch. - Лучший FCP/LCP — контент доступен сразу, без ожидания гидрации.
Ограничения
- Нельзя использовать хуки состояния и эффектов.
- Нельзя подписываться на браузерные события.
- Нельзя использовать Context напрямую (только через Client Component-обёртки).
Что хочет услышать интервьюер
Понимание разницы между RSC и традиционным SSR: RSC возвращает не HTML, а сериализованное React-дерево, которое можно патчить без перезагрузки страницы
Знание ограничений Server Components: отсутствие useState, useEffect, обработчиков событий и необходимость директивы 'use client' для интерактивных частей
Понимание паттерна композиции (children/slots) для совмещения серверных и клиентских компонентов без нарушения границ
Осознание выгод: сокращение JS-бандла, прямой доступ к серверным ресурсам, параллельный фетчинг данных на сервере
Понимание требования сериализуемости пропсов при передаче данных из Server в Client Component
Пример: Серверный компонент с прямым запросом к БД
// app/users/page.tsx — серверный компонент по умолчанию в App Router
import { db } from '@/lib/db';
export default async function UsersPage() {
// Этот код выполняется только на сервере — БД-credentials не утекут в браузер
const users = await db.user.findMany({
select: { id: true, name: true, email: true },
take: 20,
});
return (
<ul>
{users.map((user) => (
<li key={user.id}>
{user.name} — {user.email}
</li>
))}
</ul>
);
}
Пример: Граница Server/Client и паттерн children
'use client';
// components/Collapsible.tsx — клиентский компонент с состоянием
import { useState } from 'react';
export function Collapsible({ children }: { children: React.ReactNode }) {
const [open, setOpen] = useState(true);
return (
<section>
<button onClick={() => setOpen((v) => !v)}>
{open ? 'Свернуть' : 'Развернуть'}
</button>
{open && children}
</section>
);
}
// app/dashboard/page.tsx — серверный компонент-оркестратор
import { Collapsible } from '@/components/Collapsible';
import { RevenueChart } from './RevenueChart'; // тоже серверный
export default async function DashboardPage() {
// Данные фетчатся параллельно на сервере
const [stats, revenue] = await Promise.all([
fetchStats(),
fetchRevenue(),
]);
return (
<Collapsible>
{/* Server Component передаётся как children — допустимо */}
<RevenueChart data={revenue} />
</Collapsible>
);
}
Типичные ошибки
Путать RSC с SSR: думать, что Server Components — это просто компоненты, отрендеренные в HTML на сервере, тогда как RSC имеют особый wire-формат и поддерживают стриминг и частичные обновления
Пытаться импортировать Server Component внутрь Client Component напрямую, вместо использования паттерна children/slots
Передавать несериализуемые данные (функции, классы) как пропсы из Server Component в Client Component
Добавлять 'use client' везде «на всякий случай», теряя все преимущества архитектуры и увеличивая бандл
Не понимать, что контекст React (Context API) недоступен в Server Components — для глобального состояния нужны Client Component-обёртки


