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-комплектов, а не для простых блоков.