React
Паттерны компонентов
Компонент Modal начинался с трёх пропсов. Через год у него их тридцать: showHeader, headerIcon, footerButtons, onSecondaryClick, hideCloseButton и так далее. Каждое новое требование добавляет ещё один флаг, внутри растёт лес условий, а вызвать его правильно невозможно без чтения исходника. Это классический тупик конфигурации. Паттерны компонентов предлагают другой выход: вместо нагромождения пропсов дать пользователю собирать поведение из кусочков. Те же библиотеки, которыми пользуются ежедневно - Radix, React Router, формы - построены именно так.
- Headless-библиотеки Radix UI и React Aria: compound components вроде Select.Trigger и Select.Content вместо десятков пропсов
- Таблицы и списки, где строку и ячейку задаёт render prop, а данные приходят сверху
- Дизайн-системы, где Tabs, Accordion и Menu собираются из под-компонентов, а не настраиваются флагами
- Разделение container и presentational в крупных приложениях ради тестируемости и переиспользования вёрстки
- Кастомные хуки как способ вынести логику списка или формы и переиспользовать её в разных представлениях
Предварительные знания
- Компоненты, пропсы и проп children
- Кастомные хуки и вынос логики из компонента
- useContext для передачи значения вниз по дереву
От миксинов к хукам: эволюция переиспользования
Переиспользование логики в React прошло несколько эпох. Сначала были миксины классов, которые путали источники методов и конфликтовали именами. На смену пришли render props и компоненты высшего порядка (HOC): логика выносилась в компонент, а результат отдавался через функцию-ребёнка или обёртку. У HOC обнаружились свои беды - ад вложенности обёрток и неясное происхождение пропсов. В 2019 хуки (React 16.8) дали прямой способ делить логику без лишних узлов в дереве. Сегодня render props и compound components остаются для композиции разметки, а логику чаще всего выносят в кастомные хуки.
Compound components и композиция вместо конфигурации
Когда компонент обрастает десятками булевых пропсов, это сигнал конфигурационного тупика: каждое требование добавляет флаг, внутри множатся условия, а API становится непрозрачным. Compound components предлагают обратный подход - разбить компонент на семейство связанных под-компонентов, которые вместе образуют целое и делят общее состояние через Context.
Под-компоненты не получают активную вкладку через пропсы - они читают её из общего Context, который заводит корневой Tabs. Разметку и порядок задаёт вызывающий, а синхронизацию состояния берёт на себя семейство. Добавить иконку или произвольный элемент между вкладками можно прямо в JSX, не трогая API компонента.
Композиция вместо конфигурации это общий принцип за этим паттерном. Гибкость даётся через вложенность и children, а не через рост числа пропсов. Поэтому Radix UI и React Aria экспонируют именно семейства под-компонентов, оставляя разметку и стиль на усмотрение приложения.
Какую проблему решают compound components по сравнению с компонентом на множестве пропсов?
Container и presentational, render props
Разделение container и presentational отделяет два разных дела. Презентационный компонент отвечает только за то, как это выглядит: получает данные и колбэки пропсами и рисует разметку, не зная, откуда данные взялись. Контейнер отвечает за то, что показывать: грузит данные, держит состояние и передаёт всё вниз. Презентационный компонент чисто тестируется и переиспользуется, потому что не привязан к источнику данных.
Render props решают другую задачу - переиспользование логики при свободе разметки. Компонент инкапсулирует поведение, а как отрисовать результат, решает вызывающий через функцию-ребёнка (children как функция). Один и тот же источник данных можно показать списком, таблицей или карточками, не дублируя логику.
С приходом хуков многие задачи, которые раньше решались render props и HOC, удобнее решать кастомным хуком: логика выносится без лишних узлов в дереве. Render props остаются хороши там, где наружу нужно отдать именно управление разметкой конкретного элемента.
В чём смысл разделения на container и presentational компоненты?
Паттерн кастомного хука и headless-подход
Паттерн кастомного хука доводит идею разделения до предела: вся логика - состояние, эффекты, обработчики - выносится в хук, а компонент остаётся почти чистой отрисовкой. В отличие от render props и HOC, хук не добавляет узлов в дерево и не порождает обёрток. Логику можно переиспользовать в любом представлении, просто вызвав хук.
Это основа headless-подхода: библиотека отдаёт логику и состояние хуком, а всю разметку и стиль пишет приложение. Так устроены React Hook Form (хук useForm) и многие компоненты Radix и TanStack: поведение и доступность внутри, внешний вид целиком на стороне разработчика.
- Render props / HOC — Логика выносится в компонент, результат отдаётся функцией-ребёнком или обёрткой. Добавляет узлы в дерево, возможна вложенность обёрток.
- Кастомный хук — Логика выносится в хук без узлов в дереве. Переиспользуется вызовом, не порождает обёрток. Дефолтный способ делить логику сегодня.
Паттерны не самоцель. Compound components и render props оправданы, когда компонент действительно переиспользуется в разных формах. Для одноразового UI они добавляют сложность без выгоды. Принцип композиции вместо конфигурации применяют там, где гибкость реально нужна, а не везде подряд.
Чем паттерн кастомного хука выигрывает у render props и HOC для переиспользования логики?
Связь с другими темами
Этот урок про паттерны композиции. Рядом стоят соседние темы:
- Кастомные хуки — Базовый механизм выноса логики, на котором держится паттерн кастомного хука и headless-компоненты
- TypeScript и React — Дженерик-компоненты и типизация children делают эти паттерны безопасными в большой кодовой базе
Итог
- Compound components разбивают сложный компонент на семейство под-компонентов, общающихся через Context, вместо десятков пропсов
- Разделение container и presentational отделяет логику и данные от чистой вёрстки, упрощая тесты и переиспользование
- Render props и children-as-function передают наружу управление разметкой: компонент даёт данные, а как их показать решает вызывающий
- Паттерн кастомного хука выносит логику в хук, оставляя представлению только отрисовку - основа headless-подхода
- Композиция вместо конфигурации это сквозной принцип: собирать поведение из частей предпочтительнее, чем накапливать булевы флаги
- Современные библиотеки (Radix, React Aria) построены на этих паттернах, поэтому их узнавание ускоряет работу с экосистемой
Связанные уроки
- rc-16-custom-hooks — Паттерн кастомного хука это базовый способ вынести логику, на котором стоят остальные паттерны
- rc-41-typescript-react — Типизация дженерик-компонентов и пропсов делает эти паттерны безопасными в больших кодовых базах