EverProduct
Фронтенд

Этап 04 · Приложение

Лего, а не скульптура: мышление компонентами

Разбиение интерфейса на части выглядит вопросом форматирования, а на деле это проектирование маленького языка, на котором твоя команда будет говорить годами.

Два разработчика делают один и тот же экран.

Первый пишет его одним файлом: шапка, фильтры, таблица, действия над строкой, пагинация, пустое состояние. Работает, пятьсот строк, и любое будущее изменение начинается с чтения всех пятисот в поисках места.

Второй пишет страницу, которая читается фразой: шапка, панель фильтров, таблица, пагинация, — и каждый из этих кусков описан в другом месте и понятен сам по себе. Тоже работает. Файл страницы — тридцать строк.

Разница не в опрятности. Разница в том, что второй может изменить панель фильтров, не читая таблицу, проверить пустое состояние, не опустошая базу, и во вторник отдать пагинацию в другой проект. Всё дальше — о том, как получить вторую версию намеренно.

Компонент — функция из данных в интерфейс

Убери синтаксис — и компонент окажется функцией: он берёт данные, пропсы, и возвращает описание того, что должно быть на экране. Те же пропсы на входе — тот же результат.

Такая формулировка отвечает на большинство вопросов новичка. Почему компонент не может изменить собственные пропсы? Потому что функция не переписывает свои аргументы: это данные вызывающего. Откуда берутся данные? Сверху, их передали. Как ребёнок влияет на родителя? Вызывая функцию, которую родитель ему дал.

Это и есть поток, на котором держится вся модель: данные идут вниз, события идут вверх. Изменение происходит в одном месте — в компоненте, владеющем этим состоянием, — и всё ниже перерисовывается от нового значения. Двусторонняя связь между произвольными компонентами — то, чего ты избегаешь, и стоит знать почему: когда что угодно может изменить что угодно, вопрос «кто выставил это значение?» становится безответным ровно тогда, когда ответ нужен.

Где проводить границы

Указание «разбей на компоненты» бесполезно без критерия, и новички применяют его в обе неверные стороны — один огромный компонент или сорок компонентов, где кнопка завёрнута в обёртку, завёрнутую в контейнер.

Четыре признака того, что нечто заслуживает стать компонентом:

  • Оно повторяется. Одна и та же визуальная вещь встречается дважды или чаще.
  • Оно понятно само по себе. Ты можешь описать, что это, одной фразой, не упоминая окружение.
  • Оно владеет состоянием. У него есть «открыто/закрыто», текущая вкладка, черновое значение.
  • Оно заменяемо. Реализацию можно подменить так, что родитель не заметит.

И признак в обратную сторону: если извлечение требует передать восемь пропсов, существующих лишь для восстановления контекста, из которого оно только что вышло, шов проведён не там. Каждое извлечение стоит читателю прыжка; пусть оно себя окупает.

Применимое правило большого пальца — ограничение рабочей памяти из сферы «Как учиться»: если то, что делает компонент, не помещается в голове целиком, он слишком большой. Это не про количество строк, а про количество независимых происходящих вещей. Сто строк прямолинейной разметки — нормально; сорок строк, жонглирующих четырьмя несвязанными заботами, — нет.

Композиция лучше конфигурации

Вот самый частый способ, которым проектирование компонентов сворачивает не туда, и происходит это постепенно.

Ты пишешь карточку. Потом кому-то нужна карточка с картинкой — добавляешь проп. Потом без рамки, потом с подвалом, потом чтобы заголовок был ссылкой, потом с иконкой, потом компактная. Через полгода карточка принимает четырнадцать пропсов, девять из которых булевы, а её тело — заросли условий, которые никто не станет разгребать.

Альтернатива — позволить вызывающему передавать содержимое, а не описывать его: дети, слоты, именованные области. Вместо hasFooter, footerText и footerAlign карточка принимает подвал, а вызывающий кладёт туда что хочет. Карточка перестаёт нуждаться в знании обо всех случаях, потому что больше их не решает.

Правило: пропсы — для данных и небольшого числа вариантов; композиция — для структуры. Поймал себя на добавлении четвёртого булева — это и есть сигнал: не пятый добавить, а открыть слот.

Пропсы — это API

Именование и проектирование пропсов заслуживают большего внимания, чем обычно получают, потому что ты пишешь не деталь реализации: ты проектируешь маленький язык, которым другие люди — включая тебя через три недели — будут пользоваться, не читая исходник.

Несколько вещей окупаются всегда. Называй по смыслу, а не по внешнему виду: variant="danger" переживёт редизайн, variant="red" нет. Предпочитай один проп status трём взаимоисключающим булевым, которые позволяют выразить недопустимые комбинации. Давай разумные умолчания, чтобы частый случай был коротким. И держи поверхность маленькой: каждый проп — обещание продолжать работать.

Запах, за которым стоит следить, — прокидывание пропсов: значение передают через четыре компонента, которые им не пользуются, лишь бы добраться до пятого. Один-два раза это нормально и проще альтернативы. Постоянно — значит, это значение на самом деле общее состояние, и его место в контексте или хранилище, о чём и будет следующая статья.

Пропсы компонента — это API. Его главный пользователь — ты сам через три недели, всё забывший.

Списки и ключи

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

Дай каждому элементу устойчивую идентичность — идентификатор из твоих данных, — и фреймворк сможет понять, что элемент переехал, а не что у него изменилось содержимое. Возьми вместо этого индекс массива — и при перестановке, вставке или удалении фреймворк сопоставит не те элементы: состояние, привязанное к строке, уедет к другой строке, поле сохранит текст от чужой записи, а галочка появится там, где ты её не ставил.

Правило короткое: устойчивый идентификатор из данных, никогда индекс, если только список не фиксирован и не переупорядочивается.

Переиспользуемое не значит хорошее

В этой профессии есть перекос в сторону раннего обобщения, и стоит он дороже, чем дублирование.

Проблема такая: ты выносишь общий компонент, увидев два похожих случая, а третий оказывается почти таким же — и получает флаг. Четвёртый получает ещё один. Скоро общий компонент превращается в лабиринт условий, обслуживающий четыре вещи, которые никогда по-настоящему не были одинаковыми, и любое изменение ради одной угрожает трём остальным.

Две эвристики, которые держатся. Дождись третьего случая перед обобщением: два — совпадение, три — паттерн с видимой формой. И дублирование дешевле неверной абстракции: скопированный код легко удалить и легко менять независимо, а неверная абстракция — узел, затягивающийся с каждым новым случаем.

Другая сторона дизайн-системы

Если в проекте есть дизайн-система, её компоненты — тот же объект, который описывает статья про дизайн-системы в сфере «UX-дизайн», увиденный со стороны кода. Токены — общие значения, компоненты — общие решения.

Отчего библиотека компонентов оказывается договорённостью, а не папкой: что такое кнопка, какие бывают варианты, что содержит карточка. Когда это работает, «возьми вторичную кнопку» от дизайнера и твой variant="secondary" — одна и та же фраза, и целый класс работы «сделай как в макете» исчезает.

Практичным это делает сборка компонентов в изоляции — в специальном стенде или просто на черновой странице, где рядом отрисованы все состояния. Ты видишь их разом, включая те, до которых в настоящем приложении трудно добраться.

На практике

Разбей существующую страницу на бумаге до кода. Нарисуй коробки и назови каждую. Если для описания коробки требуется «и», это две коробки.

Собери один компонент с намеренно плохим API, потом пересобери хорошо. Десять булевых против слотов, на той же карточке. Прочувствовать оба варианта ценнее правила, с которым ты согласился, — это практика на границе, а не повторение того, что уже умеешь.

Считай булевы пропсы. Четыре — точка, в которой вместо пятого открывают слот.

Отрисуй все состояния рядом на черновой странице: пусто, загрузка, ошибка, один элемент, много, длинный текст, отсутствующая картинка. Пять минут — и находятся случаи, которые иначе доезжают до прода.

Выращивай личную библиотеку. Через несколько месяцев у тебя должны быть свои версии повторяющихся кусков: кнопка, поле, модалка, таблица, пустое состояние, уведомление. Это твои чанки на этом этапе, и строятся они так же, как все чанки: каждый собирается несколько раз, в разных контекстах, с проверкой результата.

Перечитай компонент, написанный месяц назад, и попробуй воспользоваться им, не открывая тело. Не получилось — чинить надо API, а не память.

Проверь себя

Закрой статью и ответь своими словами:

  1. Почему компонент не может изменить собственные пропсы и что это заменяет?
  2. Назови четыре признака того, что нечто должно стать компонентом, и один — что не должно.
  3. В чём практическая разница между конфигурацией пропсами и композицией слотами?
  4. Почему variant="danger" лучше, чем variant="red"?
  5. Что такое прокидывание пропсов и когда оно перестаёт быть приемлемым?
  6. Что ломается при использовании индекса массива в качестве ключа?
  7. Почему дублирование иногда дешевле общего компонента?

Коротко

  • Компонент — функция из пропсов в интерфейс; данные текут вниз, события вверх, и именно это оставляет вопрос «кто это изменил?» отвечаемым.
  • Разделяй там, где нечто повторяется, описывается само по себе, владеет состоянием или заменяемо, — и останавливайся, когда компонент перестаёт помещаться в голове целиком.
  • Конфигурируй пропсами данные и варианты, композицией — структуру, а четвёртый булев проп считай сигналом к переходу.
  • Пропсы — публичный API: называй по смыслу, предпочитай один статус нескольким булевым, давай умолчания, держи поверхность маленькой.
  • Прокидывание значения через несколько слоёв — признак того, что это общее состояние, а не проп.
  • Ключи должны быть устойчивыми идентификаторами из данных; индексы заставляют фреймворк сопоставлять не те элементы при изменении списка.
  • Обобщай на третьем случае и помни, что дублирование дешевле неверной абстракции.
  • Компоненты — кодовая половина дизайн-системы, а сборка в изоляции со всеми видимыми состояниями делает эту договорённость настоящей.