Compound Components

Compound Components представляют собой паттерн проектирования, широко применяемый в библиотеке Preact и других реактивных фреймворках. Основная идея заключается в разделении логики компонента и его визуальных частей, позволяя создавать гибкие и расширяемые интерфейсы, где отдельные подкомпоненты могут взаимодействовать с общим состоянием родителя.

Основные принципы

  1. Контейнер и дочерние элементы Родительский компонент выступает как контейнер, который управляет состоянием и поведением. Дочерние компоненты получают доступ к этому состоянию через контекст или специальные пропсы. Такой подход устраняет необходимость пробрасывания пропсов через несколько уровней вложенности (prop drilling).

  2. Разделение ответственности Контейнер отвечает за бизнес-логику, состояние и управление событиями. Дочерние компоненты фокусируются на отображении данных. Это делает код более читаемым и поддерживаемым.

  3. Гибкость и расширяемость Паттерн позволяет добавлять новые дочерние компоненты без изменения внутренней логики родителя. Каждый дочерний элемент может быть полностью настраиваемым, что упрощает повторное использование.

Пример реализации

Рассмотрим создание компонента Tabs, где есть несколько вкладок и переключение между ними.

import { h } from 'preact';
import { useState, createContext, useContext } from 'preact/hooks';

const TabsContext = createContext();

function Tabs({ children, defaultIndex = 0 }) {
    const [activeIndex, setActiveIndex] = useState(defaultIndex);

    return (
        <TabsContext.Provider value={{ activeIndex, setActiveIndex }}>
            <div className="tabs">{children}</div>
        </TabsContext.Provider>
    );
}

function TabList({ children }) {
    return <div className="tab-list">{children}</div>;
}

function Tab({ index, children }) {
    const { activeIndex, setActiveIndex } = useContext(TabsContext);
    const isActive = activeIndex === index;

    return (
        <button
            className={`tab ${isActive ? 'active' : ''}`}
            onCl ick={() => setActiveIndex(index)}
        >
            {children}
        </button>
    );
}

function TabPanels({ children }) {
    const { activeIndex } = useContext(TabsContext);
    return <div className="tab-panels">{children[activeIndex]}</div>;
}

export { Tabs, TabList, Tab, TabPanels };

В этом примере:

  • Tabs — контейнер, управляющий состоянием активной вкладки.
  • TabList — визуальный обертка для кнопок вкладок.
  • Tab — отдельная вкладка, которая получает индекс и проверяет активна ли она.
  • TabPanels — отображает содержимое только активной вкладки.

Передача состояния через контекст

Использование createContext и useContext позволяет дочерним компонентам автоматически получать доступ к состоянию родителя без передачи пропсов через каждый уровень. Это особенно важно при создании сложных интерфейсов, где количество вложенных компонентов может быть большим.

const TabsContext = createContext();

Контекст содержит:

  • activeIndex — текущий выбранный индекс вкладки.
  • setActiveIndex — функция для изменения активной вкладки.

Преимущества Compound Components

  • Сокрытие внутренней логики: пользователи компонента не нуждаются в знании внутренних деталей реализации.
  • Повторное использование: отдельные подкомпоненты могут использоваться в других местах приложения.
  • Упрощение тестирования: каждый дочерний компонент можно тестировать независимо от родителя.
  • Лёгкая кастомизация: стили и элементы интерфейса можно изменять без вмешательства в логику состояния.

Расширенные возможности

Compound Components позволяют создавать более сложные сценарии, такие как:

  • Вкладки с динамическим содержимым и асинхронной подгрузкой данных.
  • Формы с вложенными полями и валидацией.
  • Аккордеоны, где каждая секция управляется единым состоянием родителя.

Применение этого паттерна упрощает управление состоянием, делает интерфейсы модульными и облегчает сопровождение больших приложений на Preact.

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

  • Всегда выделять контейнерный компонент для управления состоянием.
  • Дочерние компоненты должны быть максимально «тонкими», фокусируясь на визуальном представлении.
  • Контекст — основной механизм связи между контейнером и дочерними элементами.
  • Использовать индекс или уникальные ключи для идентификации подкомпонентов при взаимодействии.

Этот подход обеспечивает чистую архитектуру, повышает гибкость компонентов и сокращает количество повторяющегося кода.