Compound components

Урок про compound components (составные компоненты): паттерн, где набор связанных компонентов делит общее состояние через контекст и собирается декларативно, как родные теги HTML.

Compound components — группа компонентов, спроектированных работать вместе (как <select> и <option>): родитель держит состояние, дети читают его неявно, а пользователь компонует их свободно.

Проблема, которую решает паттерн

Представьте компонент Tabs. Наивный API — куча пропсов: <Tabs labels={[…]} contents={[…]} active={…} onChange={…} />. Он быстро становится негибким: нельзя вставить иконку в заголовок, изменить порядок, добавить разделитель. Compound-подход переворачивает API — пользователь сам выкладывает структуру:

<Tabs defaultActive="profile">
  <Tabs.List>
    <Tabs.Tab id="profile">Профиль</Tabs.Tab>
    <Tabs.Tab id="settings">Настройки</Tabs.Tab>
  </Tabs.List>
  <Tabs.Panel id="profile">Данные профиля</Tabs.Panel>
  <Tabs.Panel id="settings">Форма настроек</Tabs.Panel>
</Tabs>

Как это устроено: общий контекст

Родитель Tabs держит активную вкладку в состоянии и кладёт её (и сеттер) в контекст. Дети Tab и Panel читают контекст и сами решают, как себя вести. Внешнему коду не нужно знать про состояние — оно скрыто.

const TabsContext = React.createContext(null);

function Tabs({ defaultActive, children }) {
  const [active, setActive] = React.useState(defaultActive);
  const value = React.useMemo(() => ({ active, setActive }), [active]);
  return <TabsContext.Provider value={value}>{children}</TabsContext.Provider>;
}

function Tab({ id, children }) {
  const { active, setActive } = React.useContext(TabsContext);
  return (
    <button
      aria-selected={active === id}
      onClick={() => setActive(id)}
    >
      {children}
    </button>
  );
}

function Panel({ id, children }) {
  const { active } = React.useContext(TabsContext);
  return active === id ? <div>{children}</div> : null;
}

// привязываем как статические свойства — отсюда синтаксис Tabs.Tab
Tabs.List = function List({ children }) { return <div role="tablist">{children}</div>; };
Tabs.Tab = Tab;
Tabs.Panel = Panel;

Чем хорош паттерн

  • Гибкая разметка. Пользователь свободно вкладывает, оборачивает, переставляет дочерние элементы.
  • Чистый API. Нет «простыни» пропсов; связь между частями скрыта в контексте.
  • Инкапсуляция состояния. Логика активной вкладки внутри Tabs, наружу не торчит.

Цена и когда применять

Паттерн добавляет неявную связь через контекст: Tabs.Tab бессмыслен вне Tabs (стоит бросить понятную ошибку, если контекст null). Применяйте его для переиспользуемых UI-комплектов с несколькими взаимосвязанными частями: табы, аккордеоны, меню, степперы. Для одноразового блока с двумя пропсами это избыточно.

Итог

  • Compound components делят общее состояние через контекст и собираются декларативно.
  • Родитель хранит состояние; дети читают его неявно и сами управляют поведением.
  • Подвязывайте части как Parent.Child через статические свойства.
  • Паттерн для гибких переиспользуемых UI-комплектов, а не для простых блоков.
Проверьте себя
1. Как связанные части compound-компонента делят состояние?
AЧерез глобальные переменные
BЧерез общий контекст, который держит родитель
CЧерез localStorage
DПередавая всё пропсами вручную
2. Зачем привязывают подкомпоненты как Tabs.Tab, Tabs.Panel?
AЭто требование React
BЧтобы дать декларативный, читаемый API и сгруппировать связанные части
CЧтобы ускорить рендер
DЧтобы избежать использования контекста
3. Когда compound components избыточны?
AДля табов и аккордеонов
BДля одноразового блока с парой пропсов
CДля переиспользуемых UI-комплектов
DДля меню со многими частями
Поддержать проект