Проектирование 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 и не держать состояние многословнее в месте вызова, зато приложение становится единственным источником истины и может управлять выбором, сбрасывать или сохранять его. Решение принадлежит документации, одна модель на компонент, чтобы следующий вызывающий знал, передавать значение или начальную точку.