Привет! Меня зовут Антон Крылов, я фронтенд-разработчик в Авито — занимаюсь профилями. До Авито я работал тимлидом и видел много хорошего и плохого кода. В этой статье — мои наблюдения и размышления на тему, как сделать компонент, который приятно использовать и который не придётся переписывать через время. Прежде чем говорить о том, как написать хороший компонент, давайте поговорим о том, как делать точно не нужно. Компонент можно назвать сложным, если: У него нет тестов. Такой компонент без будет тяжело использовать извне, а расширять — ещё сложнее.  Он делает слишком много. Я встречал компоненты на 1000–2000 строк, которые умеют всё. Но чтобы что-то поправить в таком компоненте, может понадобиться целый спринт. Он Overengineered. Разработчики часто грешат добавлением искусственных сложностей в код. Например:  useEffect вместо useCallback. Я периодически встречаю такой кейс, когда в одном месте обрабатываются действия после изменения state, которое происходит где-то глубоко в компонентах фильтра. Почитайте, почему это не лучшее решение. RxJS(подставьте сюда любую технологию с которой сложно разобраться) везде. Это классная штука, но иногда она не нужна. Например, для сингл-кейса лучше написать императивный код. Лишние слои абстракции. Иногда их плодят, чтобы весь код переиспользовался как можно больше, а в итоге потребности в переиспользовании нет.  Чтобы понять, как работает такой искусственно усложнённый компонент, придётся прочитать весь код. Это долго и неэффективно. Это компонент, чьё состояние или пропсы не соответствуют ожидаемому поведению.  Неконсистентность возникает, если компонент изменяет свое состояние в ответ на действия пользователя, но не обновляет соответствующие данные в других компонентах, которые зависят от него. Это может привести к непредсказуемому поведению или сбоям. Проблему неконсистетности можно решить на этапе дизайна API для компонента. Допустим, мы написали компонент TextField, а потом захотели расширить его валидацией. Можно сделать так: Но такое решение — неконсистентное. Если по ошибке передать в TextField только один из пропов, ничего работать не будет. Ещё вариант — добавить валидацию пропов, но и это не лучшее решение. В таком варианте только один вариант для ошибки — добавить новые не получится: Лучшее решение для консистентности — пользоваться правилом «single prop — single logic» — на каждый проп должна быть своя логика: Есть ещё два признака компонентов, которые сложно использовать и тем более расширять: использование отображения на флагах и чересчур большое количество пропсов — Props Hell. Отображение на флагах. Проблема в том, что рано или поздно количество флагов разрастётся. И тогда будет тяжело разобраться, что происходит в коде.  Если без флагов никак не обойтись — делайте маппинг.  Props Hell — это ситуация, в которой компонент принимает большое количество пропсов, которые должны передаваться от родительского компонента. Props Hell обычно приводит к низкой поддерживаемости и сложности в изменении и тестировании компонента, потому что разбираться в таком тяжело и долго: Чтобы избежать Props Hell, можно разбить компонент на более мелкие или использовать контекст.  Вообще, прежде чем писать компонент, нужно понять, зачем мы его делаем. Разработчики сами должны решать, что хорошо, а что плохо. Это напрямую зависит от того, насколько решение попадает в текущие требования и изменяется под будущие. Для этого стоит разобраться, какие требования к компонентам существуют, и на что нужно ориентироваться при написании кода. Функциональные требования к компонентам говорят о том, что они должны уметь делать и как должны выглядеть.  Функциональные требования обычно формулирует бизнес и дизайнеры. Первым нужно, чтобы изменения приносили больше денег, а вторым — чтобы это было красиво и удобно.  Есть ещё требования от разработки — чтобы компоненты были простые и понятные. Но на самих себя, к сожалению, разработчики часто забивают и концентрируются на бизнес-требованиях. Так делать не стоит. Нефункциональные требования касаются как раз разработчиков. Они о том, как сделать компонент простым, понятным и лёгким для переиспользования.  Вот какие нефункциональные характеристики есть у компонентов: Гибкость — насколько компонент способен адаптироваться к различным сценариям использования, не нарушая своей функциональности. Гибкий компонент можно переиспользовать в разных контекстах и с разными данными без большого количества изменений в коде. Например, компонент TextField из примера выше — негибкий: слишком большое количество пропсов усложняют модификацию. Коробочность — насколько просто пользоваться компонентом извне. Допустим, у нас есть компонент Table, который принимает только URL. Передавать данные таблицы через URL неудобно — это плохое решение для внешнего API. Но это может быть и хорошим решением — например, для CRUD-таблицы, где все данные ± однотипны. Зависимость — насколько компонент зависим от других компонентов. Если у компонента много зависимостей, он менее гибкий и более хрупкий. Явность — насколько глубоко нужно знать бизнес-логику или быть погружённым в проект, чтобы работать с компонентом. Допустим, у нас есть компонент PrimaryButton. Для того, чтобы понять, что происходит, нужны знания о дизайн-системе: например, что из себя представляет variant='primary' Если хочется сделать код более явным, лучше избегать таких ситуаций. Например, вместо variant='primary' можно использовать более очевидный вариант — прокинуть код цвета: Функциональность — сколько возможностей предоставляет компонент. Например, у нас есть компонент BasicForm, который и отрисовывает форму, и умеет отправлять данные по нажатию Submit.  В каждой ситуации важность нефункциональных требований разная, и зависит от конкретных бизнес-требований. Поэтому хороший компонент — это компонент с набором характеристик, который соответствуют конкретным запросам и сценариям использования.  Допустим, у нас есть UI-кит. Рассмотрим, как характеристики будут меняться в зависимости от ситуации. Ситуация 1: UI-кит используется во многих проектах. Так обычно и бывает: чаще всего наборы компонентов используются в большом количестве проектов.  Тогда возникают такие нефункциональные требования: Переопределение стилей должно быть возможным только на уровне темы — для единообразия и согласованности дизайна. В наборе должно быть как можно меньше зависимостей — чтобы UI-кит меньше весил. API должно быть максимально простым и явным — чтобы им легко мог пользоваться даже стажёр, который пока мало знает про бизнес-требования. Ситуация 2: UI-кит используется в ограниченном количестве проектов. Допустим, мы точно знаем, что набор не будет использоваться в множестве проектов. Тогда нефункциональные характеристики поменяются: Можно позволить более высокую гибкость, поскольку в небольшом количестве проектов проще отслеживать согласованность интерфейсов.  Функциональности должно быть больше, чтобы на уровне конкретного проекта приходилось делать как можно меньше. Явность API не так важна — важнее функциональность.  У хорошего компонента характеристики соответствуют сценарию его использования. Давайте разберёмся, как влиять на нефункциональные требования, чтобы «подгонять» их под запросы для каждого конкретного компонента.  Писать тесты в любом случае. Они влияют на все характеристики, хотя и не напрямую. Например, в процессе написания тестов придётся учитывать зависимости — то есть лишний раз задуматься о них. Использовать Dependency Injection (DI), если нужны гибкость и независимости. Dependency Injection позволяет вынести создание объектов и управление их зависимостями за пределы класса — это делает код более гибким и расширяемым.  Кроме того, DI позволяет создавать более тестируемый код, так как мы можем легко заменить реальные зависимости на заглушки в тестах. Вернёмся к примеру с компонентом TextField. В нём много потенциальных мест для использования Dependency Injection. Например, вместо пропса handleEnter можно прокинуть функцию, а вместо useSuperFocus — хук. Dependency Injection в React осуществляет функция inject. Подробнее об этом можно почитать в GitHub-репозитории react-ioc. Также могут пригодиться: Расширение vscode-dependency-cruiser для VS Code — для генерации графа зависимостей из любого JS-файла Библиотека inversify-react — для внедрения зависимостей в компоненты в React-приложениях Применять DSL-like подход, если нужны коробочность и явность. Он поможет уменьшить количество кода, необходимого для создания пользовательского интерфейса, и сделать разработку более доступной и понятной для новичков. Например, можно оставить общее поведение формы BasicForm (из примера выше) в хуках: В такой ситуации можно легко сделать хоть 1000 таких форм — каждый раз переписывать их не придётся. 1. Плохой компонент — сложный и неконсистентный. 2. У компонентов есть характеристики — нефункциональные требования: гибкость; коробочность; зависимость; явность; функциональность. 3. У хорошего компонента характеристики соответствуют запросам и требованиям. 4. На характеристики можно и нужно влиять, чтобы «подгонять» их под запросы и требования. Для этого можно использовать: тесты; Dependency Injection; DSL-like подход. Моя статья на русском: Гайд по написанию и рефакторингу компонентов, которые хочется переиспользовать → Статьи на английском: You Might Not Need An Effect → Don't useEffect as callback → 5 Common mistakes to avoid when using react Hooks →  GitHub-репозитории: Использование функции inject в React → Расширение vscode-dependency-cruiser для VS Code — для генерации графа зависимостей из любого JS-файла → Библиотека inversify-react — для внедрения зависимостей в компоненты в React-приложениях → Предыдущая статья: Разработка — всё? Действительно ли нас всех заменят роботы?  Senior Frontend Engineer