🔥

Тред (Лена Рашкован)


Доброго субботнего утречка! Эта неделя наконец-то заканчивается, и многие скоро вздохнут с облегчением, и я в том числе.

Последний свой год в Яндексе я занималась созданием и развитием библиотеки компонентов образовательных сервисов, сначала по пятницам, а потом фултайм. О том, какие требования предъявляются к таким компонентам и о подходах к их проектированию будет этот тред

Эта движуха началась из-за того, что 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

У меня даже интерактивчик остался)
notion image