Logo Craft Homelab Docs Контакты Telegram
React Server Components — серверный рендеринг
Sun Dec 14 2025

React Server Components: архитектура и применение

React Server Components (RSC) — парадигма, позволяющая рендерить компоненты на сервере, снижая bundle size и улучшая производительность. RSC позволяют обращаться к базе данных напрямую из компонента, не создавая API endpoints для каждого запроса.

Основы

React Server Components работают по принципу разделения компонентов на серверные и клиентские. Это разделение позволяет оптимизировать производительность приложения.

Server vs Client Components

В Next.js 15 все компоненты в app directory по умолчанию являются серверными. Для интерактивности нужно явно указывать клиентские компоненты.

// Server Component (по умолчанию в app directory)
// - Рендерится на сервере
// - Может обращаться к БД, файловой системе
// - Не включает интерактивность
// - Не имеет доступа к hooks (useState, useEffect)

async function PostList() {
    const posts = await db.posts.findMany();
    
    return (
        <ul>
            {posts.map(post => (
                <li key={post.id}>{post.title}</li>
            ))}
        </ul>
    );
}

// Client Component
// - Рендерится на клиенте (или сервере для SSR)
// - Может использовать hooks
// - Интерактивный

'use client';

import { useState } from 'react';

function LikeButton({ postId }) {
    const [likes, setLikes] = useState(0);
    
    return (
        <button onClick={() => setLikes(l => l + 1)}>
            Like ({likes})
        </button>
    );
}

Использование

RSC позволяют создавать гибридные приложения, где тяжёлые компоненты рендерятся на сервере, а интерактивные — на клиенте.

App Directory (Next.js 13+)

Структура проекта в Next.js 15 использует app directory для определения серверных и клиентских компонентов:

app/
├── page.tsx           # Server Component
├── layout.tsx         # Server Component
├── loading.tsx        # Server Component
├── error.tsx          # Server Component
└── components/
    ├── Client.tsx     # 'use client'
    └── Server.tsx     # Server Component
// app/page.tsx - Server Component
import { db } from '@/lib/db';
import LikeButton from '@/components/LikeButton';

export default async function Page() {
    const posts = await db.posts.findMany();
    
    return (
        <main>
            <h1>Posts</h1>
            {posts.map(post => (
                <article key={post.id}>
                    <h2>{post.title}</h2>
                    <LikeButton postId={post.id} />
                </article>
            ))}
        </main>
    );
}

Server Actions

// app/actions.ts
'use server';

import { db } from '@/lib/db';
import { revalidatePath } from 'next/cache';

export async function createPost(formData: FormData) {
    const title = formData.get('title') as string;
    
    await db.posts.create({ title });
    
    revalidatePath('/');
    return { success: true };
}

// app/page.tsx
import { createPost } from './actions';

export default function Page() {
    return (
        <form action={createPost}>
            <input name="title" />
            <button type="submit">Create</button>
        </form>
    );
}

Server Functions

// lib/actions.ts
'use server';

export async function getData() {
    const data = await fetchExpensiveData();
    return data;
}

// app/page.tsx
import { getData } from '@/lib/actions';

export default async function Page() {
    const data = await getData();
    return <div>{data}</div>;
}

Streaming

Suspense

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

function Posts() {
    return (
        <ul>
            {(await db.posts.findMany()).map(post => (
                <li key={post.id}>{post.title}</li>
            ))}
        </ul>
    );
}

function PostsSkeleton() {
    return <div>Loading...</div>;
}

export default function Page() {
    return (
        <Suspense fallback={<PostsSkeleton />}>
            <Posts />
        </Suspense>
    );
}

Streaming SSR

// next.config.js
module.exports = {
    experimental: {
        serverActions: {
            bodyParser: true,
        },
    },
};

parallel routes с Suspense

// app/@modal/(.)photo/[id]/page.tsx
import { Dialog } from '@/components/Dialog';

export default function PhotoModal({ params }) {
    return (
        <Dialog>
            <img src={`/photos/${params.id}`} />
        </Dialog>
    );
}

Паттерны

Композиция Server и Client

// Server Component
import ClientWrapper from './ClientWrapper';

export default function Page() {
    return (
        <ClientWrapper>
            {/* Server Component может рендерить Client Components */}
            <ServerChild />
        </ClientWrapper>
    );
}

// Client Component
'use client';

import { useState } from 'react';

export default function ClientWrapper({ children }) {
    const [count, setCount] = useState(0);
    
    return (
        <div onClick={() => setCount(c => c + 1)}>
            {children}
            <p>Clicks: {count}</p>
        </div>
    );
}

Passing Server Data to Client

// Server Component
import ClientChart from './ClientChart';

export default async function Page() {
    const data = await fetchData(); // Server-side fetch
    
    return <ClientChart initialData={data} />;
}

// Client Component
'use client';

import { useState } from 'react';

export default function ClientChart({ initialData }) {
    const [data] = useState(initialData);
    
    return <div>{/* Render chart */}</div>;
}

Data Fetching Patterns

// Direct DB access в Server Component
async function PostPage({ params }) {
    const post = await db.post.findUnique({
        where: { id: params.id }
    });
    
    return <article>{post.content}</article>;
}

// Caching
export const revalidate = 60; // Пересоздавать каждые 60 секунд

export async function generateStaticParams() {
    const posts = await db.post.findMany();
    return posts.map(post => ({ id: post.id }));
}

Оптимизация

Передача больших данных

// Используйте промежуточные компоненты для больших данных
// Вместо передачи всего через props

// Хорошо
async function PostList() {
    const posts = await db.posts.findMany();
    return (
        <ul>
            {posts.map(post => (
                <PostItem key={post.id} post={post} />
            ))}
        </ul>
    );
}

async function PostItem({ post }) {
    // Дополнительная логика
    return <li>{post.title}</li>;
}

Client Components только там, где нужно

// Минимизируйте использование 'use client'
// Выносите интерактивность в отдельные компоненты

// Плохо
'use client';

export default function Post({ post }) {
    return (
        <article>
            <h1>{post.title}</h1>
            <LikeButton postId={post.id} />
        </article>
    );
}

// Лучше
// Post - Server Component
// LikeButton - Client Component с 'use client'

Gotchas

Ограничения

  • Нельзя использовать hooks в Server Components
  • Нельзя использовать browser APIs
  • Props должны быть сериализуемы
  • Один root Client Component = entire subtree клиентский

Когда использовать Server Components

  • Data fetching
  • Accessing backend resources
  • Large dependencies (reduce bundle)
  • Sensitive information (keys, tokens)

Когда использовать Client Components

  • Interactivity (onClick, onChange)
  • State and lifecycle (useState, useEffect)
  • Browser-only APIs
  • Custom hooks

Заключение

RSC меняют подход к React-разработке. Правильное разделение на Server и Client компоненты значительно улучшает производительность.