JavaRush /Курсы /Модуль 4: Node.js, Next.js и Angular /Композиция компонентов и лучшие практики

Композиция компонентов и лучшие практики

Модуль 4: Node.js, Next.js и Angular
7 уровень , 9 лекция
Открыта

1. Что такое композиция компонентов?

Если говорить простыми словами, композиция компонентов — это способ собирать интерфейс из маленьких, независимых и переиспользуемых деталей. Это как строить конструктор LEGO: из одних и тех же кубиков можно собрать космический корабль, дом или даже робота-пылесоса. Каждый компонент отвечает за свой кусочек интерфейса, а вместе они образуют сложную, но управляемую структуру.

В Next.jsReact вообще) композиция — это основной способ организации кода. Вы не пишете "монолитные" страницы, а делите их на логические части: кнопки, формы, карточки, списки, 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.

1
Задача
Модуль 4: Node.js, Next.js и Angular, 7 уровень, 9 лекция
Недоступна
Персональная визитка
Персональная визитка
1
Задача
Модуль 4: Node.js, Next.js и Angular, 7 уровень, 9 лекция
Недоступна
Коллекция книг с интерактивным фильтром
Коллекция книг с интерактивным фильтром
1
Задача
Модуль 4: Node.js, Next.js и Angular, 7 уровень, 9 лекция
Недоступна
Галерея с карточками и раскрывающимися подробностями
Галерея с карточками и раскрывающимися подробностями
1
Задача
Модуль 4: Node.js, Next.js и Angular, 7 уровень, 9 лекция
Недоступна
Каталог фильмов с фильтрацией и подробной карточкой
Каталог фильмов с фильтрацией и подробной карточкой
1
Задача
Модуль 4: Node.js, Next.js и Angular, 7 уровень, 9 лекция
Недоступна
Мини-дашборд студента с навигацией и редактируемым профилем
Мини-дашборд студента с навигацией и редактируемым профилем
3
Опрос
Что такое Server Components, 7 уровень, 9 лекция
Недоступен
Что такое Server Components
Что такое Server Components
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ