Урок 04

Server и Client Components

В App Router компоненты по умолчанию — серверные. Это ключевое отличие Next.js от обычного React.

Server Components (по умолчанию)

Выполняются только на сервере. В браузер отправляется готовый HTML, а не JS компонента.

// app/posts/page.tsx
export default async function Posts() {
    const res = await fetch('https://api.example.com/posts');
    const posts = await res.json();

    return (
        <ul>
            {posts.map((p) => <li key={p.id}>{p.title}</li>)}
        </ul>
    );
}

Плюсы:

  • Прямой доступ к БД, файлам, переменным окружения
  • Меньше JS в браузере
  • Быстрее первая загрузка
  • Можно async/await прямо в компоненте

Ограничения:

  • Нет useState, useEffect
  • Нет обработчиков событий (onClick)
  • Нет браузерных API (window, localStorage)

Client Components

Выполняются в браузере. Помечаются директивой 'use client':

'use client';

import { useState } from 'react';

export default function Counter() {
    const [count, setCount] = useState(0);

    return (
        <button onClick={() => setCount(count + 1)}>
            {count}
        </button>
    );
}

Когда нужны:

  • Хуки (useState, useEffect, useRef)
  • Обработчики событий
  • Браузерные API
  • Контекст (useContext)

Разделение

Не весь компонент должен быть клиентским. Выноси интерактивную часть:

// app/page.tsx — серверный
import LikeButton from './LikeButton';

export default async function Post() {
    const post = await getPost();

    return (
        <article>
            <h1>{post.title}</h1>
            <p>{post.body}</p>
            <LikeButton postId={post.id} />
        </article>
    );
}
// app/LikeButton.tsx — клиентский
'use client';

export default function LikeButton({ postId }: { postId: number }) {
    const [liked, setLiked] = useState(false);
    return <button onClick={() => setLiked(!liked)}>{liked ? '❤️' : '🤍'}</button>;
}

Серверный рендерит контент, клиентский — интерактив.

Передача данных

Серверный → клиентский через пропсы:

// Серверный
<LikeButton postId={post.id} initialLiked={post.liked} />

Пропсы должны быть сериализуемыми — без функций, классов, символов.

Клиентский внутри серверного

Можно вкладывать. Наоборот — нет (нельзя импортировать серверный в клиентский).

// Серверный
import ClientComponent from './Client';

export default function Page() {
    return (
        <div>
            <h1>Серверная часть</h1>
            <ClientComponent />
        </div>
    );
}

children в клиентском

Можно передавать серверный контент как children:

// ClientWrapper.tsx
'use client';

export default function ClientWrapper({ children }: { children: React.ReactNode }) {
    const [open, setOpen] = useState(false);
    return (
        <div>
            <button onClick={() => setOpen(!open)}>Toggle</button>
            {open && children}
        </div>
    );
}
// page.tsx — серверный
<ClientWrapper>
    <ServerContent />
</ClientWrapper>

Серверный контент рендерится на сервере, клиентский — управляет.

Правила

  • По умолчанию — серверные
  • 'use client' — только когда нужны хуки или события
  • 'use client' влияет на весь файл и его импорты
  • Не импортируй серверный в клиентский
  • Пропсы серверный → клиентский — сериализуемые

Итоги

  • Server Components — по умолчанию
  • Меньше JS, прямой доступ к данным
  • Client Components — для интерактива
  • 'use client' наверху файла
  • Разделяй: контент на сервере, интерактив на клиенте