Compound components pattern

Compound components — это архитектурный подход, при котором один компонент выступает контейнером для нескольких вложенных компонентов, обеспечивая согласованное взаимодействие между ними. Такой подход позволяет создавать гибкие, переиспользуемые интерфейсы с понятной структурой.

В Chakra UI этот паттерн реализуется через сочетание контекста React, пропсов children и forwardRef, что позволяет вложенным компонентам получать данные от родителя без явной передачи пропсов на каждом уровне.


Создание родительского компонента

Родительский компонент выступает центральным узлом, который управляет состоянием и логикой. Для передачи данных вложенным компонентам используется контекст:

import { createContext, useContext, useState } from "react";
import { Box, Button } from "@chakra-ui/react";

const AccordionContext = createContext();

function Accordion({ children }) {
  const [openIndex, setOpenIndex] = useState(null);

  const toggleItem = (index) => {
    setOpenIndex(openIndex === index ? null : index);
  };

  return (
    <AccordionContext.Provider value={{ openIndex, toggleItem }}>
      <Box>{children}</Box>
    </AccordionContext.Provider>
  );
}
  • createContext создаёт контекст для обмена состоянием между компонентами.
  • AccordionContext.Provider обеспечивает доступ вложенным компонентам к openIndex и toggleItem.

Вложенные компоненты

Вложенные компоненты используют контекст для взаимодействия с родителем:

function AccordionItem({ index, children }) {
  const { openIndex, toggleItem } = useContext(AccordionContext);
  const isOpen = openIndex === index;

  return (
    <Box border="1px solid" borderColor="gray.200" borderRadius="md" mb={2}>
      {children({ isOpen, toggleItem: () => toggleItem(index) })}
    </Box>
  );
}
  • children как функция: вместо обычного JSX передаём функцию, которая получает состояние isOpen и метод toggleItem.
  • Это позволяет любому количеству вложенных элементов внутри AccordionItem иметь доступ к состоянию открытия/закрытия.

Компоненты заголовка и контента

Использование Compound Components часто подразумевает разделение на Header и Panel:

function AccordionHeader({ children, onClick }) {
  return (
    <Button width="100%" textAlign="left" onCl ick={onClick}>
      {children}
    </Button>
  );
}

function AccordionPanel({ isOpen, children }) {
  return isOpen ? <Box p={4}>{children}</Box> : null;
}
  • AccordionHeader реагирует на клики и вызывает toggleItem.
  • AccordionPanel отображается только если isOpen равен true.

Пример использования

<Accordion>
  <AccordionItem index={0}>
    {({ isOpen, toggleItem }) => (
      <>
        <AccordionHeader onCl ick={toggleItem}>Пункт 1</AccordionHeader>
        <AccordionPanel isOpen={isOpen}>Содержимое 1</AccordionPanel>
      </>
    )}
  </AccordionItem>
  <AccordionItem index={1}>
    {({ isOpen, toggleItem }) => (
      <>
        <AccordionHeader onCl ick={toggleItem}>Пункт 2</AccordionHeader>
        <AccordionPanel isOpen={isOpen}>Содержимое 2</AccordionPanel>
      </>
    )}
  </AccordionItem>
</Accordion>
  • Каждое AccordionItem управляет своим состоянием через общий контекст.
  • Такой подход обеспечивает полную независимость визуальных элементов от логики, сохраняя при этом согласованное поведение.

Расширение функционала

Compound Components позволяют легко добавлять новые вложенные элементы без изменения родителя. Например, добавим иконку состояния:

import { ChevronDownIcon, ChevronUpIcon } from "@chakra-ui/icons";

function AccordionHeader({ children, onClick, isOpen }) {
  return (
    <Button width="100%" textAlign="left" onCl ick={onClick} rightIcon={isOpen ? <ChevronUpIcon /> : <ChevronDownIcon />}>
      {children}
    </Button>
  );
}
  • Иконка автоматически реагирует на состояние isOpen.
  • Можно создавать новые вложенные компоненты, например AccordionFooter или AccordionBadge, просто подключив их к контексту.

ForwardRef и Compound Components

Для интеграции с Chakra UI и внешними библиотеками удобно использовать forwardRef:

import { forwardRef } from "react";

const AccordionHeader = forwardRef(({ children, onClick, isOpen }, ref) => (
  <Button ref={ref} width="100%" textAlign="left" onCl ick={onClick} rightIcon={isOpen ? <ChevronUpIcon /> : <ChevronDownIcon />}>
    {children}
  </Button>
));
  • Позволяет получать прямой доступ к DOM-элементу, необходимый для фокусировки или анимаций.
  • Сохраняет совместимость с Chakra UI компонентами и стилями.

Преимущества подхода

  1. Инкапсуляция логики — родитель управляет состоянием, дети отвечают только за рендер.
  2. Гибкость структуры — легко менять порядок и состав вложенных элементов.
  3. Переиспользуемость — один компонент контейнер может работать с любыми вложенными элементами.
  4. Чистый интерфейс API — минимальные пропсы и отсутствие необходимости вручную прокидывать состояние вниз.

Заключение по использованию

В Chakra UI паттерн Compound Components позволяет создавать сложные UI-структуры с чистой архитектурой и минимальной связностью. Контекст React обеспечивает передачу состояния между компонентами, а использование функций-children и forwardRef делает компоненты максимально гибкими и совместимыми с Chakra UI стилями и хуками. Такой подход идеально подходит для создания аккордеонов, табов, модальных окон и любых компонентов с вложенной структурой и общим управлением состоянием.