1. Что такое композиция компонентов?
Если говорить простыми словами, композиция компонентов — это способ собирать интерфейс из маленьких, независимых и переиспользуемых деталей. Это как строить конструктор LEGO: из одних и тех же кубиков можно собрать космический корабль, дом или даже робота-пылесоса. Каждый компонент отвечает за свой кусочек интерфейса, а вместе они образуют сложную, но управляемую структуру.
В Next.js (и React вообще) композиция — это основной способ организации кода. Вы не пишете "монолитные" страницы, а делите их на логические части: кнопки, формы, карточки, списки, layout-обёртки и т.д.
Пример на пальцах
Вспомним наш учебный проект: предположим, у нас есть страница списка задач (todo-list). Можно было бы написать всё в одном компоненте, но это быстро превратится в кашу. Вместо этого мы делим страницу на части:
- TaskList — список задач
- TaskItem — отдельная задача
- AddTaskForm — форма добавления задачи
- Layout — общий шаблон страницы
Каждая часть — самостоятельный компонент, который можно использовать повторно или менять независимо от других.
Почему композиция — это круто (и жизненно необходимо)
Повторное использование
В программировании лень — двигатель прогресса. Если вы написали хороший компонент, хочется использовать его в разных местах. Например, кнопка "Удалить" или карточка пользователя могут встречаться на десятке страниц — и вам не нужно копировать код, просто импортируете компонент.
Разделение ответственности
Каждый компонент отвечает только за свой кусок. Если что-то ломается — проще найти причину. Если нужно доработать функциональность — не страшно, что поломаете весь проект.
Масштабируемость
Когда проект вырастает, композиция помогает не утонуть в коде. Добавлять новые страницы и фичи становится гораздо легче: вы просто комбинируете существующие компоненты, как кубики.
Тестируемость
Маленькие компоненты проще тестировать по отдельности. Меньше багов — меньше седых волос.
2. Как выглядит композиция в Next.js 15 (App Router)
Пример базовой композиции
Давайте рассмотрим простейший пример: страница списка задач (/app/todos/page.tsx).
// app/todos/page.tsx
import TaskList from './TaskList';
import AddTaskForm from './AddTaskForm';
export default function TodosPage() {
return (
<section>
<h1>Список задач</h1>
<AddTaskForm />
<TaskList />
</section>
);
}
Здесь мы "собираем" страницу из двух компонентов: формы и списка. Каждый из них — отдельный файл, отдельная логика.
Вложенность компонентов
Компоненты могут быть вложены друг в друга сколько угодно раз. Например, TaskList может рендерить много TaskItem:
// app/todos/TaskList.tsx
import TaskItem from './TaskItem';
export default function TaskList() {
const tasks = [
{ id: 1, text: 'Выучить Next.js', done: false },
{ id: 2, text: 'Погладить кота', done: true },
];
return (
<ul>
{tasks.map(task => (
<TaskItem key={task.id} task={task} />
))}
</ul>
);
}
А TaskItem — это маленький компонент, отвечающий только за одну задачу:
// app/todos/TaskItem.tsx
export default function TaskItem({ task }) {
return (
<li>
<span style={{ textDecoration: task.done ? 'line-through' : 'none' }}>
{task.text}
</span>
</li>
);
}
Layout и шаблоны как часть композиции
В Next.js 15 layout-компоненты (например, app/layout.tsx или app/todos/layout.tsx) — это, по сути, тоже часть композиции. Они позволяют обернуть часть приложения в общий шаблон (например, с меню или футером).
// app/layout.tsx
export default function RootLayout({ children }) {
return (
<html lang="ru">
<body>
<header>Мой Todo App</header>
<main>{children}</main>
<footer>© 2024</footer>
</body>
</html>
);
}
3. Best practices: как делать композицию правильно
Один компонент — одна ответственность
Старайтесь, чтобы каждый компонент делал что-то одно. Если компонент начинает разрастаться, возможно, его стоит разбить на более мелкие.
Плохо:
Компонент MegaTodoPage и рендерит список, и добавляет задачи, и показывает статистику, и готовит кофе.
Хорошо:
Компонент TodoPage собирает всё из отдельных компонентов: TaskList, AddTaskForm, StatsBar.
Передавайте только нужные пропсы
Не стоит передавать в компонент всё подряд. Передавайте только то, что ему реально нужно. Это делает код чище и предотвращает неожиданные баги.
// Плохо: передаём весь объект task, хотя нужен только text
<TaskItem task={task} />
// Лучше: если компоненту нужен только text
<TaskItem text={task.text} />
Используйте children для обёрток
Иногда компонент должен быть "контейнером" для других компонентов. Для этого используйте проп children.
// Компонент-обёртка
function Card({ children }) {
return <div className="card">{children}</div>;
}
// Использование:
<Card>
<h2>Заголовок</h2>
<p>Содержимое карточки</p>
</Card>
Это очень мощный паттерн, который позволяет делать гибкие обёртки для модальных окон, карточек, layout-ов и т.д.
Разделяйте Server и Client Components
В Next.js 15 важно помнить: если компоненту нужна интерактивность (например, обработка кликов, локальное состояние), он должен быть Client Component ('use client' в начале файла).
- Server Components — для загрузки данных, рендера статичного контента.
- Client Components — для интерактивности (кнопки, формы, анимации).
Пример:
// app/todos/AddTaskForm.tsx
'use client';
import { useState } from 'react';
export default function AddTaskForm() {
const [text, setText] = useState('');
function handleSubmit(e) {
e.preventDefault();
// логика добавления задачи...
}
return (
<form onSubmit={handleSubmit}>
<input
value={text}
onChange={e => setText(e.target.value)}
placeholder="Новая задача"
/>
<button type="submit">Добавить</button>
</form>
);
}
Не бойтесь делать компоненты маленькими
Чем меньше компонент — тем проще его понять, протестировать и переиспользовать. Компонент из 10 строк кода — это нормально!
Давайте компонентам осмысленные имена
Имя должно отражать суть: UserCard, TodoList, Sidebar, а не Component1, MyBlock или Qwerty.
Не злоупотребляйте пропс-дриллингом
Если приходится передавать одни и те же пропсы через много уровней, возможно, стоит вынести данные выше, использовать контекст или серверные данные.
4. Пример: сборка страницы из компонентов
Давайте соберём простую страницу задач с учётом всех best practices.
Структура файлов:
app/
todos/
page.tsx
TaskList.tsx
TaskItem.tsx
AddTaskForm.tsx
StatsBar.tsx
page.tsx:
import StatsBar from './StatsBar';
import AddTaskForm from './AddTaskForm';
import TaskList from './TaskList';
export default function TodosPage() {
return (
<section>
<h1>Мои задачи</h1>
<StatsBar />
<AddTaskForm />
<TaskList />
</section>
);
}
TaskList.tsx:
import TaskItem from './TaskItem';
export default function TaskList() {
// В реальном приложении данные могут приходить с сервера
const tasks = [
{ id: 1, text: 'Купить хлеб', done: false },
{ id: 2, text: 'Почитать лекцию', done: true },
];
return (
<ul>
{tasks.map(task => (
<TaskItem key={task.id} {...task} />
))}
</ul>
);
}
TaskItem.tsx:
export default function TaskItem({ text, done }) {
return (
<li>
<span style={{ textDecoration: done ? 'line-through' : 'none' }}>
{text}
</span>
</li>
);
}
AddTaskForm.tsx:
'use client';
import { useState } from 'react';
export default function AddTaskForm() {
const [text, setText] = useState('');
function handleSubmit(e) {
e.preventDefault();
alert(`Добавили задачу: ${text}`);
setText('');
}
return (
<form onSubmit={handleSubmit}>
<input
value={text}
onChange={e => setText(e.target.value)}
placeholder="Новая задача"
/>
<button type="submit">Добавить</button>
</form>
);
}
StatsBar.tsx:
export default function StatsBar() {
// В реальном приложении данные приходят из props или сервера
const total = 2;
const done = 1;
return (
<div>
<b>Всего задач:</b> {total} | <b>Выполнено:</b> {done}
</div>
);
}
5. Схема: как компоненты собираются вместе
Вот упрощённая схема композиции для нашей страницы:
TodosPage
├── StatsBar
├── AddTaskForm (Client Component)
└── TaskList
├── TaskItem
└── TaskItem
Пояснение:
- TodosPage — собирает всё вместе.
- TaskList — рендерит массив задач.
- TaskItem — отображает одну задачу.
- AddTaskForm — форма добавления (интерактивная, клиентская).
- StatsBar — показывает статистику.
6. Когда стоит объединять компоненты, а когда — разбивать?
Объединяйте, если:
- Компоненты тесно связаны и не будут использоваться по отдельности.
- Логика настолько простая, что выносить в отдельный файл — избыточно.
Разбивайте, если:
- Компонент становится длиннее 30–40 строк.
- В компоненте появляются свои состояния или побочные эффекты.
- Один и тот же компонент нужен в разных местах.
- Хотите протестировать компонент отдельно.
7. Типичные ошибки при композиции компонентов
Ошибка №1: Гигантский компонент-страница.
Если в файле страницы 200 строк кода, куча логики и верстки — срочно разбивайте на части! Такой код сложно читать, поддерживать и переиспользовать.
Ошибка №2: Передача "всего подряд" через props.
Не нужно передавать в компонент все данные родителя. Пусть компонент получает только то, что ему реально нужно.
Ошибка №3: Многократное дублирование кода.
Если вы копируете куски JSX из файла в файл — пора выносить их в отдельный компонент.
Ошибка №4: Забыли добавить 'use client' в интерактивный компонент.
В Next.js 15 интерактивные компоненты должны быть явно помечены как клиентские. Если забыли — useState/useEffect работать не будут, а у вас появится загадочная ошибка.
Ошибка №5: Переусложнённая вложенность.
Если у вас дерево компонентов из 10 уровней, и данные идут сверху вниз через все уровни — возможно, стоит пересмотреть архитектуру, вынести данные выше или использовать контекст.
Ошибка №6: Использование Server Component там, где нужна интерактивность.
Если вы пытаетесь повесить onClick или useState на Server Component — ничего не произойдёт. Для интерактива используйте Client Components.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ