Зачем нужно разделение на серверные и клиентские компоненты
С появлением React Server Components (RSC) в экосистеме React произошёл фундаментальный сдвиг. Раньше всё было просто: компоненты рендерились на клиенте, а SSR был лишь оптимизацией для первоначальной загрузки. Теперь компоненты разделены на два типа, и понимание различий между ними — обязательный навык для любого React-разработчика.
Server Components выполняются исключительно на сервере. Их код никогда не попадает в клиентский бандл. Client Components работают в браузере и содержат интерактивную логику. Правильный выбор между ними напрямую влияет на производительность, размер бандла и пользовательский опыт.
В этой статье мы дадим чёткие критерии выбора, разберём типичные паттерны композиции и покажем ошибки, которые допускают даже опытные разработчики. Если вы работаете с Next.js 15, рекомендуем также прочитать нашу статью Next.js 16: что нового и как использовать в production.
Что такое Server Components
Принцип работы
Server Components рендерятся на сервере и отправляют клиенту готовый HTML вместе со специальным форматом данных (RSC Payload). JavaScript-код этих компонентов не включается в клиентский бандл. Это их ключевое преимущество.
В Next.js 15 (App Router) все компоненты по умолчанию являются серверными. Вам не нужно ничего делать, чтобы создать Server Component — просто напишите обычный компонент:
// app/products/page.tsx — это Server Component по умолчанию
import {db} from "@/lib/database";
export default async function ProductsPage() {
// Прямой доступ к базе данных — без API-эндпоинтов
const products = await db.product.findMany({
orderBy: {createdAt: "desc"},
take: 20,
});
return (
<main>
<h1>Каталог товаров</h1>
<ul>
{products.map((product) => (
<li key={product.id}>
<h2>{product.name}</h2>
<p>{product.description}</p>
<span>{product.price} ₽</span>
</li>
))}
</ul>
</main>
);
}
Что можно делать в Server Components
- Напрямую обращаться к базе данных (Prisma, Drizzle, SQL)
- Читать файловую систему сервера
- Использовать серверные API и приватные ключи
- Вызывать
async/awaitпрямо в компоненте - Импортировать тяжёлые библиотеки без влияния на размер бандла
- Рендерить Markdown, подсветку синтаксиса и другие тяжёлые операции
Чего нельзя делать в Server Components
- Использовать
useState,useEffect,useRefи другие хуки состояния - Назначать обработчики событий (
onClick,onChange,onSubmit) - Использовать браузерные API (
window,document,localStorage) - Использовать контекст React (
useContext) - Импортировать CSS-in-JS библиотеки, зависящие от рантайма
Что такое Client Components
Принцип работы
Client Components — это привычные React-компоненты, которые выполняются в браузере. Чтобы объявить компонент клиентским,
добавьте директиву 'use client' в начало файла:
"use client";
import {useState, useCallback} from "react";
export function ProductFilter({categories}: { categories: string[] }) {
const [selected, setSelected] = useState<string[]>([]);
const toggleCategory = useCallback((category: string) => {
setSelected((prev) =>
prev.includes(category)
? prev.filter((c) => c !== category)
: [...prev, category]
);
}, []);
return (
<div className="flex gap-2 flex-wrap">
{categories.map((category) => (
<button
key={category}
onClick={() => toggleCategory(category)}
className={`px-3 py-1 rounded ${
selected.includes(category)
? "bg-blue-600 text-white"
: "bg-gray-200"
}`}
>
{category}
</button>
))}
</div>
);
}
Важное уточнение
Директива 'use client' не означает, что компонент рендерится только на клиенте. Client Components по-прежнему проходят
SSR на сервере для генерации начального HTML. Директива указывает на границу — всё, что импортируется в файл с
'use client', становится частью клиентского бандла и гидрируется в браузере.
Когда нужны Client Components
- Интерактивные элементы: кнопки, формы, слайдеры, модальные окна
- Компоненты с состоянием: фильтры, корзина, счётчики
- Компоненты с эффектами: анимации, таймеры, подписки на WebSocket
- Компоненты, использующие браузерные API: геолокация, камера, clipboard
Чек-лист для принятия решений
Вот текстовая схема принятия решений, которую мы используем в наших проектах React-разработки:
Компонент нужно создать
│
├─ Есть ли интерактивность (onClick, onChange)?
│ ├─ Да → Client Component ('use client')
│ └─ Нет ↓
│
├─ Используется ли useState / useEffect / useRef?
│ ├─ Да → Client Component ('use client')
│ └─ Нет ↓
│
├─ Нужен ли доступ к браузерным API?
│ ├─ Да → Client Component ('use client')
│ └─ Нет ↓
│
├─ Нужен ли прямой доступ к БД / файловой системе?
│ ├─ Да → Server Component (по умолчанию)
│ └─ Нет ↓
│
├─ Использует ли тяжёлые зависимости для рендера?
│ ├─ Да → Server Component (не попадёт в бандл)
│ └─ Нет ↓
│
└─ По умолчанию → Server Component
Правило большого пальца: если компоненту не нужна интерактивность, оставляйте его серверным. Сомневаетесь —
оставляйте серверным. Добавлять 'use client' всегда можно позже, когда появится необходимость.
Паттерны композиции
Паттерн 1: Передача серверных данных в клиентский компонент
Самый частый паттерн — получить данные в серверном компоненте и передать их как пропсы в клиентский:
// app/catalog/page.tsx — Server Component
import {db} from "@/lib/database";
import {ProductGrid} from "./product-grid"; // Client Component
export default async function CatalogPage() {
const products = await db.product.findMany();
const categories = await db.category.findMany();
return (
<main>
<h1>Каталог</h1>
{/* Серверные данные передаются как пропсы */}
<ProductGrid products={products} categories={categories}/>
</main>
);
}
// app/catalog/product-grid.tsx — Client Component
"use client";
import {useState} from "react";
export function ProductGrid({products, categories}) {
const [filter, setFilter] = useState("all");
const filtered =
filter === "all" ? products : products.filter((p) => p.category === filter);
return (
<div>
<div className="flex gap-2 mb-4">
<button onClick={() => setFilter("all")}>Все</button>
{categories.map((cat) => (
<button key={cat.id} onClick={() => setFilter(cat.slug)}>
{cat.name}
</button>
))}
</div>
<div className="grid grid-cols-3 gap-4">
{filtered.map((product) => (
<div key={product.id}>{product.name}</div>
))}
</div>
</div>
);
}
Паттерн 2: Серверный компонент как children клиентского
Это мощный паттерн, позволяющий вставлять серверный контент внутрь клиентской обёртки:
// app/layout.tsx — Server Component
import {Sidebar} from "./sidebar"; // Client Component
import {Navigation} from "./navigation"; // Server Component
export default function Layout({children}) {
return (
<Sidebar>
{/* Navigation — серверный, но передаётся как children */}
<Navigation/>
{children}
</Sidebar>
);
}
// app/sidebar.tsx — Client Component
"use client";
import {useState} from "react";
export function Sidebar({children}) {
const [isOpen, setIsOpen] = useState(true);
return (
<div className="flex">
<aside className={isOpen ? "w-64" : "w-16"}>
<button onClick={() => setIsOpen(!isOpen)}>
{isOpen ? "Свернуть" : "Развернуть"}
</button>
</aside>
<main className="flex-1">{children}</main>
</div>
);
}
Ключевой момент: Navigation рендерится на сервере, несмотря на то что передаётся внутрь клиентского Sidebar. Это
работает потому, что children — это уже готовый результат рендера, а не компонент, который нужно исполнить.




