React Server Components

React Server Components (RSC) — компоненты, которые рендерятся исключительно на сервере и не отправляют JavaScript клиенту, что сокращает bundle-size и позволяет выполнять серверные операции прямо в компоненте.

Зачем нужно

Традиционный SSR с hydration: сервер рендерит HTML, клиент загружает весь JS-код компонентов для «оживления» интерфейса. RSC идут дальше: серверные компоненты вообще не включаются в JS-bundle — браузер получает только их HTML-вывод. Это уменьшает размер бандла, устраняет водопад данных (компонент запрашивает БД напрямую) и упрощает разработку fullstack-фич.

Где используется

  • Next.js App Router (Server Components по умолчанию с Next.js 13+)
  • Компоненты, работающие с базой данных или файловой системой напрямую
  • Страницы с тяжёлыми зависимостями (парсеры markdown, PDF и т.д.), которые не нужны клиенту
  • Компоненты без интерактивности (не используют useState, useEffect, обработчики событий)

Server vs Client Components

Server Component (по умолчанию в App Router)
────────────────────────────────────────────
✓ Прямой доступ к БД, файловой системе, API
✓ Не отправляет JS клиенту
✓ Поддерживает async/await
✗ Нет useState, useEffect, useContext
✗ Нет обработчиков событий (onClick и т.д.)
✗ Нет браузерных API (window, document)

Client Component ('use client' директива)
─────────────────────────────────────────
✓ Полноценный React: хуки, события, анимации
✓ Доступ к браузерным API
✗ Нет прямого доступа к серверным ресурсам
✗ Увеличивает JS-bundle

Server Component: прямой доступ к данным

// app/products/page.tsx — Server Component по умолчанию
// Нет 'use client' → выполняется только на сервере

import { db } from '@/lib/db';

// async компонент — работает только в Server Components
async function ProductsPage() {
  // Прямой запрос к БД без API endpoint
  const products = await db.products.findMany({
    where: { published: true },
    orderBy: { createdAt: 'desc' },
    take: 20,
  });

  return (
    <main>
      <h1>Каталог</h1>
      <ul>
        {products.map(product => (
          // ProductCard — тоже Server Component
          <ProductCard key={product.id} product={product} />
        ))}
      </ul>
    </main>
  );
}

export default ProductsPage;

Client Component для интерактивности

// components/AddToCartButton.tsx
'use client'; // ← директива: этот файл — Client Component

import { useState } from 'react';

interface Props {
  productId: string;
}

export function AddToCartButton({ productId }: Props) {
  const [loading, setLoading] = useState(false);

  const handleAdd = async () => {
    setLoading(true);
    await addToCart(productId);
    setLoading(false);
  };

  return (
    <button onClick={handleAdd} disabled={loading}>
      {loading ? 'Добавляем...' : 'В корзину'}
    </button>
  );
}
// app/products/[id]/page.tsx — Server Component
import { AddToCartButton } from '@/components/AddToCartButton';

async function ProductPage({ params }) {
  const product = await db.products.findUnique({ where: { id: params.id } });

  return (
    <article>
      <h1>{product.name}</h1>
      <p>{product.description}</p>
      {/* Server Component включает Client Component */}
      <AddToCartButton productId={product.id} />
    </article>
  );
}

Частые ошибки

  • Импорт серверного кода в Client Componentimport { db } from '@/lib/db' в 'use client' файле вызовет ошибку; серверные зависимости нельзя использовать на клиенте.
  • Попытка передать несериализуемые данные как props — между Server и Client Component передаются только сериализуемые значения (не функции, не классовые экземпляры).
  • Лишние 'use client' директивы — отмечают файл как Client Component не нужно, если он не использует хуки или события; это только увеличивает bundle.

Связанные темы

Ресурсы