Доброго субботнего утречка! Эта неделя наконец-то заканчивается, и многие скоро вздохнут с облегчением, и я в том числе.
Последний свой год в Яндексе я занималась созданием и развитием библиотеки компонентов образовательных сервисов, сначала по пятницам, а потом фултайм. О том, какие требования предъявляются к таким компонентам и о подходах к их проектированию будет этот тред
Эта движуха началась из-за того, что ui-либа, которая была тогда, не справлялась с задачами кастомизации. Когда был один дизайн на весь Яндекс, было ок, но число сервисов росло, и красить их всех одним цветом был не вариант
Мы внутри себя поресерчили, как такие компоненты лучше писать, эта тема меня очень увлекла, и по итогам я сделала доклад на митапе (вот ссылка, но я там очень стесняюсь, be gentle)
youtube.com/watch?v=9fEBZf…
Основное требование к компонентам в библиотеке — гибкость и возможность настроить под свой сервис. У сервисов есть общие интерактивные обучающие механики, но разная аудитория: ученикам начальной школы свой дизайн, старшеклассникам — свой.
Разработчики общеяндексовой либы решали параллельно те же проблемы, и в итоге мы все стали придерживаться одного подхода и использовать github.com/bem/bem-react
Эта штука — пушка. Лучше всего она проявляет себя на больших сложных компонентах, но я попробую рассказать, какие проблемы она решает, на несложном примере компонента-раскрывашки.
Раскрывашка умеет открываться и показывать контент и закрываться, скрывая его. Это основа компонента, причина для его существования и общая часть: это поведение нужно всем потребителям.
А дальше начинается вариативность: выглядит компонент у всех по-разному. Значит, выделяем внешний вид в отдельный hoc – «модификатор». На стороне сервиса компонент будет выглядеть как withThemeOlolo(base)
На сервисе две темы? Не беда: compose(withThemeOne, withThemeTwo)(base). Модификатор темы «включится», если в собранный компонент передать проп theme c нужным значением. Нужные стили импортируются внутри модификатора: так в сборку попадает только реально используемое в сервисе.
Аналогично с логикой. Можно научить раскрывашку залипать при прокрутке: на сервисе она может использоваться, чтобы скрывать много текста, который может не влезть в экран, и тогда должна остаться возможность ее закрыть.
Нужно ли это всем потребителям? Нет. Должен ли им приезжать лишний код, а на компоненте висеть лишние обработчики? Тоже нет.
Выносим логику в хок, получаем compose(withSticky, compose(withThemeOne, withThemeTwo))(base).
Так можно разделять не только логику, но и код для разных платформ (для десктопов не должны приезжать тачи, а для тачей, например, хаверы).
Сейчас, правда, появились мутанты-дескпады и смешали все карты)
Мы так под конец разработки узнали, что наши компоненты будут использоваться не только на ноутбуках и планшетах, но и на интерактивных досках в школах))
Еще одно преимущество такого подхода — легко проводить продуктовые эксперименты, просто заменив нужный кусочек
У всего есть трейдофф, и в данном случае это перформанс. Нужно искать баланс в количестве хоков, но программирование вообще про баланс
Такая пушка, конечно, нужна не всем.
Есть ли другие подходы к проектированию реиспользуемых гибких компонентов? Чуть позже расскажу :)
Наверняка вы видели монолитные компоненты с пропсами на два экрана, которые потом щупальцами рассовывались по его внутренностям.
Если у вас есть такой, то, возможно, вам пригодится паттерн «составные компоненты»
А может и нет, я не ваша мама)
Этот паттерн, как и многие хорошие апи, использует инверсию контроля. Наглядный пример — любой перебирающий метод массива. Вы пишете arr.sort и делегируете выбор алгоритма сортировки методу, но контроль, как именно сортировать остается у вас
Было бы странно, если бы sort пытался покрыть всевозможные сценарии использования. Как насчет отсортировать массив животных по количеству ног? Добавим эту логику внутрь и назовем опцию ‘byLegCount’, вдруг пригодится кому
Тут я технично оставлю ссылочку на статью восхитительного @kentcdodds
kentcdodds.com/blog/inversion…
Этот паттерн можно объяснить как «торчать кишками компонента наружу», давая к ним прямой доступ. Например, так:
<Layout>
<Layout.Header className="page-header">Заголовок</Layout.Header>
<Layout.Sidebar>Сайдбар</LayoutSidebar>
</Layout>
Чтобы подчеркнуть, что внутренние компоненты имеют смысл только внутри внешнего, их часто именуют через точку, добавляя их в статические свойства основного компонента
Эта идея не новая, конечно, это уже сто лет есть в HTML. select+option, details+summary, ul+li, вот это все. Торжество композиции, красивое лаконичное апи
Рулит внутренним состоянием основной компонент и обогащает своим состоянием детей, например, через контекст. Более подробно и с примерами кода, опять же, у Кента. Кент💙
kentcdodds.com/blog/compound-…
Я очень люблю этот подход, и если его применить к месту, получается прямо супер
У меня как-то была задача сделать компонент координатной плоскости. У плоскости может быть сетка, оси, на ней могут быть всевозможные графики, точки, области и сверху еще и интерактивность над ними
А может и не быть) Понимаете? Контроль, над тем что будет, — у потребителя. Как-то так:
<CoordinateSystem width={500} height={500}>
<Grid step={2} />
<XAxis />
<YAxis />
<FnGraph fn="cosX" color="red" />
</CoordinateSystem>
Апи наглядное, а все лишнее скрыто от глаз
Состояние плоскости (текущий масштаб, шкалы по осям, размеры) распространяются через контекст или через дополнительные пропсы в React.Children
У меня даже интерактивчик остался)