Композиция, slots, controlled/uncontrolled API, discriminated unions, polymorphism и устойчивые публичные контракты.
Хороший компонент делает правильное использование простым, а незаконное — непредставимым или хотя бы немедленно диагностируемым.
Вы спроектируете публичный API без boolean-взрыва, скрытой связи с приложением и утечки DOM-деталей.
До начала достаточно props, children, state ownership и строгие TypeScript unions. В конце должен появиться не конспект, а доступный набор Alert, Dialog и Select primitives с типовыми контрактами и управляемыми вариантами.
Компонент получает один связный набор обязанностей. Visual primitive знает семантику, layout и состояния взаимодействия; feature знает данные, права и команды. Импорт API-клиента в Button или Dialog связывает библиотеку с приложением. Публичный контракт оценивают по изменчивости: что должно меняться вместе, а что независимо.
Рабочая проверка. Отделите подтверждающий Dialog от команды удаления записи.
Типичная поломка. Сделать DeleteUserDialog, который сам читает route, auth и cache в общем UI package.
Флаги primary, danger, compact, loading, success быстро создают десятки сочетаний, часть которых бессмысленна. Взаимоисключающие режимы выражают variant, а состояния с разными обязательными props — discriminated union. Независимые аспекты можно оставить отдельными.
Следующий фрагмент показывает минимальный рабочий контракт, а не заготовку для слепого копирования.
type NoticeProps =
| { kind: 'info'; message: string }
| { kind: 'error'; message: string; retry?: () => void }
| { kind: 'success'; message: string; receiptId: string };retry и receiptId доступны только там, где имеют смысл; потребитель не может передать receipt для info.
Рабочая проверка. Преобразуйте три взаимно исключающих boolean props в один union.
Типичная поломка. Разрешить одновременно error и success и выбирать победителя порядком if.
Reusable primitive может поддерживать controlled open + onOpenChange и uncontrolled defaultOpen. Внутренний helper выбирает владельца один раз; переключение режима после mount не поддерживается. Callback сообщает следующий state и причину, если потребителю важно отличать Escape, trigger и outside interaction.
Рабочая проверка. Проверьте controlled Dialog, где родитель отклоняет закрытие при несохранённом draft.
Типичная поломка. Смешать open и локальный state так, что UI расходится с prop.
Named slots подходят, когда структура фиксирована. Compound components дают гибкую вложенную разметку с общим context, например Tabs.List/Trigger/Content. Context должен хранить только координацию primitive, а не весь feature state. Compound API документирует допустимую структуру и проверяет отсутствие provider понятной ошибкой.
Рабочая проверка. Соберите Tabs из List, Trigger и Content с одним внутренним контекстом selection.
Типичная поломка. Использовать cloneElement и неявно модифицировать произвольных children.
ComponentPropsWithoutRef<"button"> помогает наследовать нативные props, но spread order важен. Защищённые role/type/aria нельзя случайно перезаписать. Для Button разумно задать type="button" по умолчанию, иначе внутри формы он submit. Event callbacks можно скомпоновать, не теряя consumer handler.
Следующий фрагмент показывает минимальный рабочий контракт, а не заготовку для слепого копирования.
type ButtonProps = React.ComponentPropsWithoutRef<'button'> & { tone?: 'normal' | 'danger' };
function Button({ tone = 'normal', type = 'button', ...props }: ButtonProps) {
return <button {...props} type={type} data-tone={tone} />;
}Явный type после spread защищает выбранный контракт; если consumer должен его менять, значение уже извлечено и передано осознанно.
Рабочая проверка. Поместите Button в форму и докажите, что обычное действие не отправляет её.
Типичная поломка. Безусловно spread-ить props после внутренних aria/event props.
as с ограничениямиPolymorphic component позволяет выбрать element type, но усложняет вывод props, refs и семантику. Button, отрендеренный как div, не получает клавиатурное поведение автоматически. Часто безопаснее отдельные Button и Link primitives или slot pattern с проверенной семантикой. API as оправдан в низкоуровневой библиотеке с тестами типов.
Рабочая проверка. Сравните типы и keyboard behavior Button, LinkButton и чрезмерно общего as API.
Типичная поломка. Разрешить as="div" и считать data-role эквивалентом button.
Shared component library — зависимость. Переименование prop, смена default, DOM-структуры или timing callback может быть breaking change. Контракт фиксируют type tests, interaction tests, accessibility tests и visual review. Deprecated adapter даёт миграционный путь, но не должен жить бесконечно.
Рабочая проверка. Добавьте новый variant без изменения defaults существующих потребителей и напишите migration note.
Типичная поломка. Считать изменение DOM wrappers внутренним, хотя consumer CSS и aria relationships от него зависят.
До реализации напишите несколько usage examples: простой случай, controlled, async, error и доступность. Если обычный сценарий требует boilerplate, а опасный — короткий, API перевёрнут. Проверяйте illegal states, ownership и escape hatches. Не добавляйте prop ради единственного потребителя, пока композиция решает задачу.
Рабочая проверка. Проведите API review Dialog на пяти сценариях и удалите props, заменяемые children/actions.
Типичная поломка. Проектировать API только из внутренней реализации компонента.
Переработайте перегруженный Modal с 14 props в composable Dialog API. Отделите trigger/content/actions, controlled open state, destructive confirmation и асинхронное закрытие. Сохраните семантику и focus contract.
Критерий завершения: невалидные сочетания props отклоняются TypeScript, бизнес-данные остаются у feature, а базовый компонент проверяется независимо. Сначала зафиксируйте наблюдаемое поведение тестом или измерением, затем меняйте реализацию.
Вопросы ещё не добавлены
Вопросы для этой подтемы ещё не добавлены.
Далее: Reducers, Context и внешние хранилища