1. Почему важно разделять Server и Client Components
В Next.js 15 (и App Router в частности) вся архитектура построена вокруг разделения компонентов на две большие категории: серверные и клиентские. Это не просто прихоть разработчиков фреймворка — за этим стоит цель сделать приложения максимально быстрыми, безопасными и удобными для поддержки.
Server Components отлично подходят для работы с базами данных, файловой системой, секретами и прочими вещами, которые нельзя или не нужно отправлять в браузер. Они рендерятся на сервере, а результат — уже готовая разметка и данные — отправляется клиенту.
Client Components нужны для интерактивных элементов: кнопок, форм, выпадающих списков, управления состоянием и т.д. Они рендерятся и "оживают" уже в браузере пользователя.
Но что делать, если часть данных приходит с сервера, а часть логики — на клиенте? Как передать данные между этими мирами? Как не запутаться в потоках данных и не получить головную боль вместо красивого приложения? Вот тут и начинается магия передачи пропсов между Server и Client Components.
Как устроена передача пропсов между Server и Client Components
Основное правило
Server Component может рендерить Client Component и передавать ей пропсы.
Client Component не может рендерить Server Component.
Это похоже на эстафету: серверный компонент может "передать палочку" клиентскому, но не наоборот. Причина проста: сервер не знает о состоянии клиента, а клиент не может исполнить код, который должен выполняться только на сервере (например, обращение к базе данных).
Схема взаимодействия
[Server Component]
|
v
[Client Component]
Пояснение:
- Server Component: получает данные (например, из базы), формирует пропсы.
- Client Component: получает эти пропсы, отображает их, добавляет интерактивность.
2. Практический пример: список задач с кнопкой "Выполнить"
Давайте продолжим развивать ваше учебное приложение. Пусть у вас есть серверный компонент, который получает список задач с сервера, и клиентский компонент, который умеет отмечать задачи как выполненные (например, по клику).
Server Component: получает данные и рендерит Client Component
// app/tasks/page.tsx (Server Component)
import { getTasks } from '@/lib/data'; // функция для получения задач с сервера
import TaskList from './TaskList';
export default async function TasksPage() {
const tasks = await getTasks(); // Получаем задачи на сервере
return (
<section>
<h1>Список задач</h1>
<TaskList tasks={tasks} /> {/* Передаем задачи в клиентский компонент */}
</section>
);
}
Пояснение
- getTasks — это функция, которая, например, читает задачи из базы данных или файла.
- TaskList — это наш клиентский компонент (о нем чуть ниже).
Client Component: получает пропсы и добавляет интерактивность
// app/tasks/TaskList.tsx
'use client';
import { useState } from 'react';
export default function TaskList({ tasks }) {
// Для примера: локальное состояние для отметки задач как выполненных
const [completed, setCompleted] = useState({});
function handleComplete(id) {
setCompleted(prev => ({ ...prev, [id]: true }));
}
return (
<ul>
{tasks.map(task => (
<li key={task.id}>
<span style={{ textDecoration: completed[task.id] ? 'line-through' : 'none' }}>
{task.title}
</span>
{!completed[task.id] && (
<button onClick={() => handleComplete(task.id)}>Выполнить</button>
)}
</li>
))}
</ul>
);
}
Обратите внимание:
- В начале файла стоит 'use client', чтобы явно указать Next.js, что компонент клиентский.
- Проп tasks приходит от серверного компонента.
- Внутри компонента мы можем использовать React-хуки, обработчики событий и всё, что доступно только на клиенте.
Как это работает?
- Серверный компонент получает данные (например, из базы данных, API или файла).
- Он рендерит клиентский компонент, передавая ему данные через обычные пропсы.
- Клиентский компонент получает эти данные как входные и может их отображать, а также добавлять интерактивность (например, кнопки, обработчики кликов).
3. Что можно и что нельзя передавать как пропсы
Можно передавать:
- Примитивы (строки, числа, boolean)
- Простые объекты и массивы (которые можно сериализовать)
- Функции, если они определены внутри Client Component (см. ниже)
Нельзя передавать:
- Функции, определённые в Server Component (они не могут "поехать" в браузер)
- Не сериализуемые объекты (например, классы, Date, Map, Set, если они не сериализованы)
- React-элементы (например, нельзя передать <div /> как пропс)
Если вы попробуете передать что-то "несъедобное" (например, функцию или нестандартный объект), Next.js покажет ошибку сериализации.
Таблица: что можно передавать
| Тип пропса | Можно? | Пример |
|---|---|---|
| string | ✅ | |
| number | ✅ | |
| boolean | ✅ | |
| массив | ✅ | |
| plain object | ✅ | |
| функция (из Server) | ❌ | |
| класс | ❌ | |
| React элемент | ❌ | |
4. Передача пропсов: типизация и best practices
Если вы используете TypeScript (а в Next.js это рекомендуется), обязательно типизируйте пропсы!
// app/tasks/TaskList.tsx
'use client';
type Task = {
id: number;
title: string;
};
type Props = {
tasks: Task[];
};
export default function TaskList({ tasks }: Props) {
// ... остальной код
}
Best practice:
- Старайтесь передавать только те данные, которые действительно нужны клиентскому компоненту.
- Не передавайте большие объекты или "сырые" данные, если можно подготовить их на сервере.
- Если нужно передать что-то сложное (например, Date), сериализуйте это в строку и обратно.
5. Передача пропсов вглубь: "проброс" через несколько уровней
Server Component может рендерить Client Component напрямую, а может — через промежуточные компоненты. Главное: вся цепочка до первого Client Component — серверная.
Пример:
// app/page.tsx (Server Component)
import Parent from './Parent';
export default async function Page() {
const data = await fetchData();
return <Parent data={data} />;
}
// app/Parent.tsx (Server Component)
import Child from './Child';
export default function Parent({ data }) {
// Можно обработать/отфильтровать данные
return <Child info={data} />;
}
// app/Child.tsx (Client Component)
'use client';
export default function Child({ info }) {
// info — это данные, пришедшие с сервера через несколько уровней
return <div>{JSON.stringify(info)}</div>;
}
Важно:
- Как только цепочка доходит до Client Component, всё, что ниже по дереву, должно быть клиентским (или вложенные клиентские компоненты).
- Client Component не может рендерить Server Component. Если попробовать — получите ошибку.
6. Ограничения и нюансы передачи пропсов
Почему нельзя рендерить Server Component внутри Client Component?
Потому что серверный компонент требует выполнения на сервере, а клиентский — уже работает в браузере. В браузере нет доступа к серверу, чтобы "досчитать" компонент на лету. Это похоже на попытку приготовить борщ в микроволновке на даче, когда все ингредиенты остались в супермаркете.
Как сделать так, чтобы Client Component получал "свежие" данные?
- Обновлять данные через пропсы (например, при навигации или перерисовке страницы).
- Использовать клиентские API (fetch, SWR, React Query) внутри Client Component, если нужно обновлять данные без перезагрузки страницы.
Передача функций как пропсов
- Server Component не может передать функцию в Client Component.
- Если нужна функция (например, обработчик клика), она должна быть определена внутри Client Component.
- Можно передавать идентификаторы, а не функции. Например, передать id задачи, а обработчик реализовать на клиенте.
7. Применение в учебном приложении
Давайте добавим в ваше приложение ещё одну фичу: фильтрацию задач по статусу (выполнено/не выполнено).
Server Component: формирует данные
// app/tasks/page.tsx
import { getTasks } from '@/lib/data';
import TaskList from './TaskList';
export default async function TasksPage() {
const tasks = await getTasks();
return (
<section>
<h1>Список задач</h1>
<TaskList tasks={tasks} />
</section>
);
}
Client Component: фильтрация и интерактивность
// app/tasks/TaskList.tsx
'use client';
import { useState } from 'react';
export default function TaskList({ tasks }) {
const [showCompleted, setShowCompleted] = useState(true);
const [completed, setCompleted] = useState({});
function handleComplete(id) {
setCompleted(prev => ({ ...prev, [id]: true }));
}
const filteredTasks = tasks.filter(
task => showCompleted || !completed[task.id]
);
return (
<div>
<label>
<input
type="checkbox"
checked={showCompleted}
onChange={e => setShowCompleted(e.target.checked)}
/>
Показывать выполненные
</label>
<ul>
{filteredTasks.map(task => (
<li key={task.id}>
<span style={{ textDecoration: completed[task.id] ? 'line-through' : 'none' }}>
{task.title}
</span>
{!completed[task.id] && (
<button onClick={() => handleComplete(task.id)}>Выполнить</button>
)}
</li>
))}
</ul>
</div>
);
}
Пояснение
- Серверный компонент передал массив задач.
- Клиентский компонент реализует фильтрацию и обработку событий.
8. Типичные ошибки при передаче пропсов между Server и Client Components
Ошибка №1: попытка передать функцию из Server Component в Client Component.
Это невозможно, потому что функция не может быть сериализована и передана в браузер. Всегда определяйте обработчики событий внутри Client Component.
Ошибка №2: попытка рендерить Server Component внутри Client Component.
Такой код не сработает и вызовет ошибку компиляции. Архитектура Next.js строго разделяет эти миры.
Ошибка №3: передача не сериализуемых объектов (например, Date, Map, Set) как пропсов.
Передавайте только простые объекты и массивы, которые можно преобразовать в JSON. Если нужно передать дату — сериализуйте её в строку, а на клиенте преобразуйте обратно.
Ошибка №4: забыли добавить 'use client' в компонент, который должен быть клиентским.
Без этой директивы компонент будет считаться серверным, и вы не сможете использовать хуки или обработчики событий.
Ошибка №5: чрезмерная вложенность Client Components, где можно обойтись Server Component.
Не делайте все компоненты клиентскими "на всякий случай". Оставляйте максимум логики и рендеринга на сервере, а клиентскими делайте только те компоненты, которым действительно нужна интерактивность.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ