Урок 18. Старый vs Новый: asChild против render
⚡ Кратко
Разница между поколениями shadcn/ui видна именно в исходниках: другой пакет примитивов, другой способ полиморфизма, другие имена состояний в атрибутах.
Полиморфизм
❌ Radix: asChild и Slot
import { Slot } from "@radix-ui/react-slot"
function Button({ asChild = false, ...props }) {
const Comp = asChild ? Slot : "button"
return <Comp {...props} />
}
// использование
<Button asChild>
<a href="/reports">Отчёты</a>
</Button>
✅ Base UI: проп render
import { Button as ButtonPrimitive } from "@base-ui/react/button"
function Button(props: ButtonPrimitive.Props) {
return <ButtonPrimitive data-slot="button" {...props} />
}
// использование
<Button render={<a href="/reports" />}>Отчёты</Button>
Оба решают одну задачу — «отрисуй меня другим тегом, сохранив поведение и стили». Но API разные, и смешивать их в одном проекте нельзя.
Состояния в атрибутах
| Что | Radix | Base UI |
|---|---|---|
| Открыт | data-state="open" | data-open |
| Закрывается | data-state="closed" | data-closed |
| Класс в Tailwind | data-[state=open]:… | data-open:… |
| Части оверлея | Overlay, Content | Backdrop, Popup |
Оформление: тогда и сейчас
| Приём | Раньше | Сейчас |
|---|---|---|
| Метки частей | не использовались | data-slot у каждой части |
| Параметры компонента | дублирование классов в вариантах | CSS-переменная и py-(--card-spacing) |
| Цвета | hsl(var(--primary)) | токены в OKLCH |
| Размер иконки | задавался вручную | [&_svg:not([class*='size-'])]:size-4 |
| Реакция на содержимое | пропсы-флаги | has-data-[slot=…] |
Общий вектор: всё больше логики оформления переезжает
в CSS-селекторы и атрибуты, а JavaScript остаётся только для поведения.
Компоненты становятся короче и предсказуемее.