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

Пример 1 — Два булевых свойства, которые складываются в бессмыслицу

Компонент кнопки принимает флаги primary, secondary и danger, и одно место вызова передаёт два сразу: скажите, что API позволяет такого, чего не должен, и замените флаги одним перечислимым значением.

function Button({ primary, secondary, danger, children }) {
  const cls = primary ? "primary" : secondary ? "secondary" : danger ? "danger" : "";
  return <button type="button" className={cls}>{children}</button>;
}

<Button primary>Сохранить</Button>
<Button primary danger>Удалить</Button>   {/* оба истинны: бессмыслица, и это компилируется */}

Решение

function Button({ variant = "secondary", children }) {
  return <button type="button" className={"btn-" + variant}>{children}</button>;
}

<Button>Отмена</Button>
<Button variant="primary">Сохранить</Button>
<Button variant="danger">Удалить</Button>

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

Пример 2 — Компонент, который иногда слушается переданного значения

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

function Tabs({ items, value, initialTab = 0 }) {
  const [internal, setInternal] = useState(value ?? initialTab);
  // следует за `value` до первого клика, потом игнорирует его
  return items.map((item, i) => (
    <button key={item} onClick={() => setInternal(i)}>{item}</button>
  ));
}

Решение

function Tabs({ items, value, onChange }) {
  return items.map((item, i) => (
    <button key={i} type="button" onClick={() => onChange(i)}>{item}</button>
  ));
}
const [tab, setTab] = useState(0);
<Tabs items={tabs} value={tab} onChange={setTab} />

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