Урок 18. Старый vs Новый: asChild против render

📁 Раздел: shadcn/ui ⏱️ Время изучения: ~55 мин 🎯 Сложность: Продвинутая

⚡ Кратко

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

Состояния в атрибутах

ЧтоRadixBase UI
Открытdata-state="open"data-open
Закрываетсяdata-state="closed"data-closed
Класс в Tailwinddata-[state=open]:…data-open:…
Части оверлеяOverlay, ContentBackdrop, Popup

Оформление: тогда и сейчас

ПриёмРаньшеСейчас
Метки частейне использовалисьdata-slot у каждой части
Параметры компонентадублирование классов в вариантахCSS-переменная и py-(--card-spacing)
Цветаhsl(var(--primary))токены в OKLCH
Размер иконкизадавался вручную[&_svg:not([class*='size-'])]:size-4
Реакция на содержимоепропсы-флагиhas-data-[slot=…]
Общий вектор: всё больше логики оформления переезжает в CSS-селекторы и атрибуты, а JavaScript остаётся только для поведения. Компоненты становятся короче и предсказуемее.