Хотите заказать веб-сайт? Связаться с нами

Собираем React-приложение из компонентов

Цель урока: понять главный принцип построения React-приложений: композиция (сборка из мелких компонентов) вместо наследования (создания иерархий классов). Научиться создавать гибкие, переиспользуемые компоненты через передачу детей (children) и пропсов-компонентов.

Собираем React-приложение из компонентов

Введение: почему композиция, а не наследование

В React нет механизма наследования компонентов. И это сделано специально.

Главная мысль:

React построен на композиции, а не на наследовании. Ты не создаёшь «родительский компонент» и не наследуешь от него. Вместо этого ты собираешь приложение из маленьких независимых кусочков, как из деталей конструктора.

Почему композиция лучше?

  1. Гибкость — можно менять части независимо
  2. Переиспользование — один компонент можно вставить в разные места
  3. Простота — каждый компонент делает одну вещь
  4. Тестируемость — легко тестировать маленькие части

Что такое композиция и наследование

Подход Что это Пример в React
Наследование Класс получает свойства и методы от другого класса ❌ Не используется в React
Композиция Компонент собирается из других компонентов ✅ Основной подход в React

Наследование (не в React):

// Это НЕ React-способ
class Animal {
  eat() { ... }
}

class Dog extends Animal {
  bark() { ... }
}

Композиция (в React):

function App() {
  return (
    <Page>
      <Header />
      <Content>
        <Article />
      </Content>
      <Footer />
    </Page>
  );
}

1 способ композиции: props.children

Базовый пример:

function Card({ children }) {
  return (
    <div className="card">
      <div className="card-content">
        {children}
      </div>
    </div>
  );
}

function App() {
  return (
    <Card>
      <h2>Заголовок</h2>
      <p>Текст внутри карточки</p>
    </Card>
  );
}

Что здесь происходит:

  1. Card — компонент-обёртка
  2. children — всё, что находится между открывающим и закрывающим тегом <Card>
  3. App решает, какое содержимое будет внутри карточки

Более сложный пример:

function Layout({ children }) {
  return (
    <div className="layout">
      <header>Шапка сайта</header>
      <main>{children}</main>
      <footer>Подвал сайта</footer>
    </div>
  );
}

function App() {
  return (
    <Layout>
      <h1>Добро пожаловать!</h1>
      <p>Это содержимое страницы</p>
    </Layout>
  );
}

2 способ композиции: специальные пропсы (не children)

Иногда нужно передать несколько «слотов» в компонент:

Пример с несколькими вставками:

function SplitPanel({ left, right }) {
  return (
    <div className="split-panel">
      <div className="left">{left}</div>
      <div className="right">{right}</div>
    </div>
  );
}

function App() {
  return (
    <SplitPanel
      left={<Sidebar />}
      right={<MainContent />}
    />
  );
}

Что здесь происходит:

  1. SplitPanel ожидает два пропса: left и right
  2. Они могут содержать любые компоненты
  3. SplitPanel решает, как их расположить
Из ноутбука появляются летающие светящиеся экраны

Оживи свой сайт. Освой JavaScript!

Статичная верстка — это только скелет. Наш онлайн-курс "JavaScript с нуля до профи" даст твоим страницам мышцы и нервы. Научись создавать слайдеры, формы, интерактивные карты и получать данные с сервера.

От теории — к реальным скриптам в твоём портфолио.

Подробнее о курсе

3 способ композиции: смешивание children и props

function Dialog({ title, actions, children }) {
  return (
    <div className="dialog">
      <h2>{title}</h2>
      <div className="dialog-body">{children}</div>
      <div className="dialog-actions">{actions}</div>
    </div>
  );
}

function App() {
  return (
    <Dialog
      title="Подтверждение"
      actions={
        <>
          <button>Отмена</button>
          <button>Подтвердить</button>
        </>
      }
    >
      <p>Вы уверены, что хотите удалить этот файл?</p>
    </Dialog>
  );
}

Реальный пример: компонент с настройками

// Базовый компонент-контейнер
function Card({ title, icon, children, footer, onClose }) {
  return (
    <div className="card">
      <div className="card-header">
        {icon && <span className="icon">{icon}</span>}
        <h3>{title}</h3>
        {onClose && <button onClick={onClose}>✕</button>}
      </div>
      
      <div className="card-body">
        {children}
      </div>
      
      {footer && (
        <div className="card-footer">
          {footer}
        </div>
      )}
    </div>
  );
}

// Использование
function App() {
  return (
    <Card
      title="Профиль пользователя"
      icon={<img src="/avatar.png" alt="Avatar" />}
      footer={<button>Сохранить</button>}
      onClose={() => console.log('Закрыто')}
    >
      <p>Имя: Алексей</p>
      <p>Email: alex@example.com</p>
    </Card>
  );
}

Когда использовать композицию (основные паттерны)

Паттерн Описание Когда использовать
Контейнер Компонент, который содержит другие компоненты Для страниц, секций, модальных окон
Специализация Создание конкретных версий общего компонента Для вариаций (например, кнопки разных цветов)
Конфигурация Передача конфигурации через пропсы Для настройки поведения и внешнего вида

Композиция как альтернатива наследованию

Проблема: хочется создать разные типы кнопок.

Наследование (плохо в React):

// ❌ Не делайте так
class Button extends React.Component { ... }
class PrimaryButton extends Button { ... }
class DangerButton extends Button { ... }

Композиция (правильный путь):

// ✅ Делайте так
function Button({ variant, children, ...props }) {
  const style = {
    primary: { color: 'white', background: 'blue' },
    danger: { color: 'white', background: 'red' },
    outline: { color: 'blue', background: 'transparent' }
  };

  return (
    <button style={style[variant]} {...props}>
      {children}
    </button>
  );
}

// Использование
function App() {
  return (
    <>
      <Button variant="primary">Сохранить</Button>
      <Button variant="danger">Удалить</Button>
      <Button variant="outline">Отмена</Button>
    </>
  );
}

Специализация через композицию

Создание конкретных компонентов из общего:

// Общий компонент
function UserCard({ user, extraInfo, children }) {
  return (
    <div className="user-card">
      <h3>{user.name}</h3>
      <p>Возраст: {user.age}</p>
      {extraInfo && <p>{extraInfo}</p>}
      {children}
    </div>
  );
}

// Специализированные версии
function SimpleUserCard({ user }) {
  return <UserCard user={user} />;
}

function DetailedUserCard({ user }) {
  return (
    <UserCard 
      user={user} 
      extraInfo={`Город: ${user.city}, Работа: ${user.job}`}
    >
      <button>Посмотреть профиль</button>
    </UserCard>
  );
}

function DeletableUserCard({ user, onDelete }) {
  return (
    <UserCard user={user}>
      <button onClick={onDelete}>Удалить пользователя</button>
    </UserCard>
  );
}

Контейнеры и презентационные компоненты

Презентационный компонент — отвечает только за отображение:

function UserProfile({ user, onEdit }) {
  return (
    <div>
      <h2>{user.name}</h2>
      <p>Email: {user.email}</p>
      <button onClick={onEdit}>Редактировать</button>
    </div>
  );
}

Контейнер — управляет данными и логикой:

function UserProfileContainer() {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    fetchUser().then(data => {
      setUser(data);
      setLoading(false);
    });
  }, []);

  const handleEdit = () => {
    // Логика редактирования
  };

  if (loading) return <p>Загрузка...</p>;
  if (!user) return <p>Пользователь не найден</p>;

  return <UserProfile user={user} onEdit={handleEdit} />;
}

Преимущества:

  1. Презентационный компонент легко переиспользовать
  2. Контейнер содержит только логику
  3. Легко тестировать оба

Композиция высшего порядка (HOC vs композиция)

В React существует HOC (Higher Order Component) — но это тоже композиция:

// HOC — компонент, который принимает компонент и возвращает новый
function withLoading(Component) {
  return function WrappedComponent({ isLoading, ...props }) {
    if (isLoading) {
      return <p>Загрузка...</p>;
    }
    return <Component {...props} />;
  };
}

// Использование
const UserListWithLoading = withLoading(UserList);
const ProductListWithLoading = withLoading(ProductList);

Важно: HOC — это тоже композиция, а не наследование!

Сравнение с наследованием (для понимания)

Аспект Наследование (не React) Композиция (React)
Отношение «Является» (is-a) «Имеет» (has-a)
Гибкость Ограничена иерархией Высокая, можно комбинировать
Переиспользование Через родительский класс Через пропсы и children
Сложность Увеличивается с глубиной Растёт линейно
Тестирование Трудно из-за зависимости Просто — компоненты изолированы

Частые ошибки новичков

Ошибка Почему Исправление
Создают иерархии компонентов через extends React не поддерживает наследование Используй композицию
Не используют children Дублируют код Передавай children
Хардкодят зависимости Компонент становится жёстким Передавай всё через пропсы
Создают сложные контейнеры Смешивают логику и отображение Разделяй на контейнер и презентацию

Итог (что мы узнали)

Что узнали Как это работает
Композиция Сборка компонентов из других компонентов
Наследование ❌ Не используется в React
children Базовый способ композиции
Пропсы-компоненты Передача компонентов через пропсы
Специализация Создание конкретных версий через пропсы
Контейнеры vs Презентация Разделение логики и отображения
HOC Композиция через обёртку

Задание для закрепления (на 10–12 минут)

Создай систему из трёх компонентов:

Компонент Modal (контейнер) с пропсами

  1. title — заголовок модалки
  2. children — содержимое
  3. actions — кнопки действий (передаются как пропс)
  4. onClose — функция закрытия

Компонент ConfirmationModal (специализация) — модалка для подтверждения

  1. Использует Modal внутри
  2. Принимает message и onConfirm
  3. Автоматически добавляет кнопки «Отмена» и «Подтвердить»

Компонент FormModal (специализация) — модалка с формой:

  1. Использует Modal внутри
  2. Принимает fields (массив полей формы)
  3. Автоматически добавляет кнопку «Отправить»

Проверка: В App.jsx отобрази обе модалки и убедись, что они работают.

Что дальше?

В следующем уроке разберём Основные хуки — useState, useEffect, useMemo, useReducer, useCallback, useRef, useContext. Ты познакомишься со всеми базовыми инструментами для управления состоянием, производительностью и побочными эффектами. Это ключевая тема для понимания современных React-приложений.

Теги: