Проектирование API компонента

Что это такое

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

Почему это важно

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

Ключевые идеи

Предпочитайте небольшой закрытый набор вариантов

Свойство variant с тремя именованными значениями документирует себя и поддаётся проверке. Три булевых значения, которые дают восемь состояний, из которых три бессмысленны, — нет.

<Button variant="primary" />
<Button variant="secondary" />
<Button variant="danger" />

Вопрос, который стоит задать каждому булеву значению: «может ли оно когда-нибудь осмысленно сочетаться с другим?» Если нет — перед вами одно перечислимое значение.

Делайте частый случай значением по умолчанию

Самое частое использование не должно требовать свойств. Если большинство мест вызова передают size="medium", сделайте medium значением по умолчанию и уберите свойство из этих мест: так и читать короче, и менять потом легче.

Слоты — для структуры, свойства — для данных

Передавайте содержимое как дочерние элементы или именованные слоты, а не как свойства, набитые разметкой. Компонент, который принимает слот footer, можно переиспользовать так, как его автор не представлял; компонент, который принимает footerHtml строкой, безопасно расширить нельзя вообще.

<Card>
  <Card.Header>Счёт №1042</Card.Header>
  <Card.Body>{lines}</Card.Body>
</Card>

Не выдавайте внутреннее устройство

Открывайте смысловые точки подключения, а не внутренние имена классов. Если DOM компонента изменится, потребителям не придётся ничего менять. Там, где доступны переменные CSS, их и стоит открывать: это намеренно маленькая и стабильная поверхность.

.badge {
  --badge-bg: navy;   /* публичное */
  --badge-pad: 0.25rem;
  background: var(--badge-bg);
  padding: var(--badge-pad);
}

Решите, кому принадлежит состояние

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

Практика

Перепишите компонент с флагами на варианты

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

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

Задокументируйте компонент, используя его

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

Готово, когда: все три примера работают, самый короткий вызов не требует свойств, и ни один пример не лезет во внутреннее имя класса.

Частые ошибки

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

Что читать дальше

  • Язык гипертекстовой разметки (HTML) (02-frontend/03-html) — семантика элементов, которые вы оборачиваете, — часть API, а не деталь реализации.
  • Композиция и темизация компонентов (02-frontend/12-ui-components/02-composing-and-theming-components) — следующий шаг: заставить несколько компонентов работать как один.
  • Дизайн-системы (03-design-ux/32-design-systems) — более широкая практика, частью которой является API компонента.