Как работает React-паттерн «Составной компонент» (compound component) и для чего он нужен
Разбираем типичные проблемы при разработке компонентов. Изучаем, какие архитектурные подходы вложены в паттерн. Реализуем паттерн на примере компонента Аккордеон и смотрим на плюсы и минусы подходов
232 открытий2К показов

Знаете про составной компонент?
Нет, мне достаточно обычный
Да, но не знаю, как применять
Да, и активно применяю
Создавая компоненты для дизайн-систем важно предусмотреть несколько вещей:
- Как безболезненно масштабировать компонент?
- Как быстро и просто доставлять изменения?
- Как сделать компонент устойчивым к рефакторингу?
Для решения этих проблем разработчики прибегают к использованию различных паттернов проектирования. Один из таких — «Составной компонент». Сегодня рассмотрим его со всех сторон и дадим рекомендации по использованию.
Дисклеймер:
- Примеры кода упрощены для наглядности и могут требовать доработки под конкретные задачи.
- Подходы в статье не являются универсальными, правильными или неправильными. Каждый из них решает конкретные задачи и проблемы, выбирайте подход согласно требованиям в вашем проекте.
- Автор под понятием дизайн-система подразумевает такие вещи, как UI-кит и библиотека компонентов .
Какие проблемы помогает решить «Составной компонент»?
При создании переиспользуемых компонентов разработчики вынуждены решать одни и те же проблемы:
- Как сделать строение компонента достаточно гибким, чтобы оно покрывало как можно больше вариантов использования? Если об этом не подумать — вокруг появятся вспомогательные компоненты и утилитарные функции.
- Как сделать API компонента устойчивым к изменениям? Если об этом не подумать props начинают обрастать пометками deprecated, появляются проблемы с обратной совместимостью старых и новых props, а время рефакторинга и выхода новой версии неизбежно приближается.
Важно помнить: даже незначительные изменения в компоненте запускают дорогой и долгий релизный цикл, который обычно включает в себя:
- Создание новых тестов и обновление старых (снапшоты, скриншоты, e2e),
- Обновление документации или сторибука,
- Проверка изменений в реальном проекте,
- Запуск CI-CD пайплайнов для доставки изменений.
…и многие другие прелести продуктовой разработки.
Все эти проблемы стремится решить «Составной компонент»
Из чего состоит «Составной компонент»?
Компонент становится «составным» при наличии следующих свойств:
Есть иерархия:
- Есть главный и дочерние компоненты,
- Дочерние компоненты зависят от главного.
Есть общая логика:
- У компонентов есть общее внутреннее состояние, например: открыть/закрыть компонент, выбрать вид отображения и т.п.
- Главный компонент содержит логику управления дочерними компонентами, например: выбрать активный элемент, раскрыть элементы, добавить элемент в список и т.п.
Нет противоречий с основными принципами SOLID:
- Принцип единственной ответственности: каждый дочерний компонент отвечает только за свою часть логики,
- Инкапсуляция состояния: состояние управляется родительским компонентом, но не требует явной передачи через props,
- Инверсии контроля: пользователь свободно комбинирует дочерние компоненты, меняет их порядок, добавляет свои элементы без внесения изменений в исходный код компонента.
Как создать свой «Составной компонент»?
Для примера сделаем компонент Аккордеона.
Но, в начале сделаем классический React-компонент через props — так мы лучше поймем преимущества одного подхода и недостатки другого:
Реализация
Применение
Результат: мы сделали классический глупый компонент, задача которого принять на вход данные, затем вернуть строго структурированный список.
Создаём «Составной компонент»
Теперь сделаем компонент на основе паттерна, подробно разобрав его архитектуру. Начнём с декомпозиции компонента на составные части:
Основной компонент
Назначение:
- Управлять своим состоянием,
- Выступать контроллером для дочерних компонентов, обеспечивая их согласованную работу.
Во избежание props-дриллинг для передачи состояния от основного компонента к дочерним будем использовать React Context API:
Дочерние компоненты
Назначение:
- Принимать логику, состояние и методы из главного компонента через React Context API
- Реагировать на изменение собственных props
Функциональная обёртка
- Помогает дочерним компонентам быть неконтролируемыми
Компонент заголовка
- Отображает заголовок и реагирует на действия пользователя
Компонент контента
- Отображает контент и реагирует на изменение состояния в ответ на действия пользователя
Теперь сложим все составные части и попробуем собрать полноценный компонент:
Что произойдет при внесении изменений в компонент?
Теперь внесём изменения в структуру компонента, добавив новые элементы. Далее сравним сложность внесения изменений в обоих подходах:
Добавим кнопку «Закрыть»
Как сработает обычный компонент
Плюсы:
- Подход очевиден, прост и интуитивно понятен,
- Положение кнопки «Закрыть» определяется один раз.
Минусы:
- Местоположение компонента жестко закреплено,
- Изменить лэйбл и навесить дополнительное событие можно только через создание новых props в основном компоненте.
Как сработает составной компонент
Плюсы:
- Компонент может свободно перемещается внутри Accordion.Item,
- На компонент можно навесить любой хэндлер без изменения props.
Минусы:
- Свобода создания структуры компонента влечёт необходимость полностью описывать все внутренние элементы, вместо обычной передачи свойств.
Добавим компонент «Разделитель»
Теперь попробуем изменить верстку, добавив разделитель. Также поменяем местами заголовок и контент:
Обычный компонент
Плюсы:
- Положение разделителя определяется один раз.
Минусы:
- Изменить расположение разделителя можно только через изменение кода основного компонента.
- Добавить новые свойства компоненту можно только через props основного компонента.
Составной компонент
Плюсы:
- Положение элемента можно свободно перемещать без изменений в основном компоненте
О каких особенностях паттерна важно знать?
Тришейкинг
Как только компонент становится свойством объекта — сборщик перестает считать неиспользуемые составные компоненты «мертвым кодом». Следовательно, такие компоненты будут всегда попадать в конечный бандл.
Next.JS
Использование подхода в среде Next.JS приводит к ошибке. Ошибка связана с разделением компонентов внутри фреймворка на клиентские и серверные. Проблема решается указанием директивы 'use client' в начале файла там, где используются составные компоненты.
Выводы
- Паттерн приносит наибольшую пользу при разработке компонентов для дизайн-систем, где обновление компонента может быть дорогим, долгим и опасным. Использование паттерна в обычном приложении может стать излишним усложнением.
- Строение компонента позволяет свободно комбинировать дочерние компоненты, изменять их порядок и добавлять новые элементы без модификации родительского кода.
- Состояние и логика управления инкапсулированы в главном компоненте, это снижает сложность конечного кода, упрощает тестирование и повышает надежность компонента.
Если React-компонент внутри дизайн-системы обладает сложной логикой, управляет внутренним состоянием, имеет зависимые дочерние компоненты, требует гибкости и кастомизации, то он — хороший кандидат для реализации с использованием паттерна «Составной компонент». Примеры таких компонентов: RadioGroup, TagGroup, Select, Dropdown, Form, Tab.
Кстати, если хотите узнать больше про React, недавно мы выпустили подборку наших материалов. Скорее смотрите!
232 открытий2К показов