Tarkibga o'tish

22 β€” Komponentlar va framework integratsiyasi

⬅️ Oldingi: 21 β€” Plaginlar ekotizimi Β· 🏠 README Β· Keyingi: 23 β€” Forma va UI komponentlari amaliy ➑️

Bu bobda: 03-bobda ko'tarilgan eng katta savolga β€” "shu 8 ta klassni har joyda takrorlaymanmi?!" β€” nihoyat to'liq, professional javob beramiz. Takrorlanishni CSS darajasida (@apply) emas, komponent darajasida hal qilishni o'rganamiz; @apply qachon o'rinli-yu qachon tuzoq ekanini ajratamiz; React/Vue/Svelte'da klass yozish naqshlarini va eng xavfli dinamik klass tuzog'ini ko'ramiz; so'ng zamonaviy asboblar β€” clsx, tailwind-merge (twMerge), cn yordamchisi va class-variance-authority (cva) β€” bilan production darajasidagi qayta ishlatiladigan <Button> quramiz. Bob oxirida siz "klass takrorlanishi" muammosidan butunlay qutulgan bo'lasiz.


22.1 Avval nega? β€” takrorlanish muammosi, to'g'ri javob bilan

03-bobda sizni bir va'da bilan qoldirgandik: utility klasslar takrorlanadi, lekin yechim siz o'ylagandek @apply emas β€” keyinroq batafsil ko'ramiz, degandik. Mana o'sha "keyinroq" yetib keldi.

Muammoni yana bir bor aniq qo'yaylik. Sizda foydalanuvchilar ro'yxati bor, har biri karta ko'rinishida. Karta klasslari β€” rounded-xl bg-white p-6 shadow-md. Savol: 20 ta foydalanuvchi uchun shu 4 ta klassni 20 marta yozasizmi?

Birinchi instinkt β€” "CSS faylda .card deb klass yasayman, @apply bilan utility'larni unga yig'aman". To'xtang. Bu deyarli har doim noto'g'ri javob. Sababini chuqur tushunish β€” bu bobning yuragi.

To'g'ri javob: takrorlanishni siz allaqachon kun bo'yi qiladigan vosita bilan hal qiling β€” abstraksiya komponent darajasida. Ya'ni siklda (.map()) yoki haqiqiy komponentda (React/Vue/Svelte) klasslar bitta joyda yashaydi, ko'p marta ishlatiladi.

πŸ’‘ Analogiya. Tasavvur qiling, bir xil xatni 20 kishiga yuborasiz. Xatni 20 marta qo'lda ko'chirib yozasizmi? Yo'q β€” bitta shablon yozasiz, pochta dasturi uni 20 marta yuboradi. Komponent ham xuddi shu: klass qatori β€” shablon, sikl yoki render uni takrorlaydi. Siz takrorlamaysiz β€” kompyuter takrorlaydi.

Mana React'da to'g'ri va noto'g'ri yondashuv yonma-yon:

// ❌ Nusxa-ko'chirish β€” klass 4 ta manbada
<div className="rounded-xl bg-white p-6 shadow-md">{users[0].name}</div>
<div className="rounded-xl bg-white p-6 shadow-md">{users[1].name}</div>
<div className="rounded-xl bg-white p-6 shadow-md">{users[2].name}</div>
<div className="rounded-xl bg-white p-6 shadow-md">{users[3].name}</div>

// βœ… Komponent + sikl β€” klass BITTA manbada
function Card({ children }) {
  return <div className="rounded-xl bg-white p-6 shadow-md">{children}</div>;
}

{users.map((u) => <Card key={u.id}>{u.name}</Card>)}

Pastki variantda rounded-xl bg-white p-6 shadow-md faqat bir joyda turibdi β€” Card ichida. Soyani shadow-md dan shadow-lg ga o'zgartirmoqchimisiz? Bitta qatorni tahrirlaysiz, hamma kartalar birga yangilanadi. Yuqoridagi noto'g'ri variantda esa 4 (yoki 20) joyni qo'lda topib tuzatasiz.

Chapdan: bir xil uzun klass qatori 4 ta kartaga nusxa-ko'chirilgan (takrorlanish); o'ngdan: klasslar bitta Card komponentida yashab, sikl orqali 4 marta render qilingan (bitta manba)

E'tibor bering β€” bu yerda hech qanday yangi CSS fayl, hech qanday @apply, hech qanday nom o'ylab topish yo'q. Klasslar HTML/JSX bilan bir joyda qoldi (ko-lokatsiya saqlandi), lekin takrorlanish yo'qoldi. Mana shu β€” utility-first'ning markaziy va'dasi.

πŸ“Œ Asosiy saboq, bir jumlada. Tailwind'da takrorlanishni shablon darajasida (komponent/sikl) hal qiling, CSS darajasida (@apply) emas.


22.2 @apply β€” qachon ha, qachon yo'q

@apply ni 20-bobda ko'rgansiz: u utility'larni oddiy CSS qoidasiga "yig'ib" beradi. Demak savol "u nima qiladi" emas β€” savol "qachon ishlataman".

Avval nega ko'pincha yo'q ekanini tushunaylik. Har bir komponentni @apply bilan CSS'da qayta qurish degani:

  • Ko-lokatsiyani yo'qotasiz. Yana HTML'dan CSS faylga sakrash, yana nom o'ylab topish β€” utility-first nimadan qutqargan bo'lsa, hammasi qaytadi.
  • Ikkinchi haqiqat manbai paydo bo'ladi. Stil endi ikki joyda: utility'lar va sizning .card qoidangiz. Ular vaqt o'tib bir-biridan uzoqlashadi.
  • Spetsifiklik va tartib muammolari. @apply bilan yasalΠ³Π°Π½ klass variantlar (hover:, md:) bilan kutilmagan tarzda kurashadi; CSS tartibi (ordering) sizdan tashqarida hal bo'ladi.

Endi qachon ha. @apply uchta tor holatda haqiqatan o'rinli:

  1. Framework'lardan mustaqil global "primitiv"lar. Loyihada React komponenti, oddiy HTML va Markdown bir vaqtda bor β€” hammasiga umumiy .btn kerak bo'lsa.
  2. Uchinchi tomon (third-party) markup'ini stillash. Siz HTML'ini o'zgartira olmaydigan kutubxona (masalan, kalendar yoki tahrirlagich) chiqargan elementlarga klass qo'sha olmaysiz β€” selektor orqali yetib borasiz.
  3. Chinakam global, kichik naqshlar. Masalan, blog matni ichidagi barcha havolalar.

Mana 3-holat β€” @apply haqiqatan to'g'ri keladigan tipik misol. Markdown'dan kelgan .prose a markup'iga siz klass qo'sha olmaysiz, lekin barcha havolalar bir xil ko'rinishi kerak:

/* app.css */
@layer components {
  .prose a {
    @apply text-indigo-600 underline underline-offset-2 hover:text-indigo-800;
  }
}

⚠️ v4 nozikligi β€” @reference. Yuqoridagi misol asosiy CSS faylida (@import "tailwindcss"; bor joyda) ishlaydi. Lekin Vue/Svelte/Astro komponentining scoped <style> bloki ichida @apply yoki theme() ishlatsangiz, u blok Tailwind kontekstini ko'rmaydi β€” shuning uchun blok boshida @reference "../app.css"; yozishingiz shart. Buni 20-bobda batafsil ko'rgansiz; bu yerda eslatib o'tamiz, chunki framework komponentlarida bu eng ko'p uchraydigan "nega ishlamayapti" sababi.

πŸ’‘ Soddacha qoida. @apply ni o'zingiz markup'ini boshqaradigan komponent uchun ishlatmang β€” u yerda komponent abstraksiyasi (22.1) deyarli har doim yaxshiroq. @apply ni faqat siz klass qo'sha olmaydigan yoki framework chegarasidan o'tadigan joyda saqlang.


22.3 Framework asoslari β€” className va class

Endi klasslarni framework'larda qanday yozishni ko'raylik. Asosiy farq sodda: React/JSX className ishlatadi (chunki class JavaScript'da band so'z), qolganlar β€” Vue, Svelte, Astro, oddiy HTML β€” class ishlatadi.

// React / JSX
<button className="px-4 py-2 bg-indigo-600 text-white rounded-lg">Tugma</button>
<!-- Vue / Svelte / Astro / HTML -->
<button class="px-4 py-2 bg-indigo-600 text-white rounded-lg">Tugma</button>

Klass β€” bu shunchaki string, demak uni dinamik qura olasiz: shablon literali (`...`), Vue'ning :class bog'lamasi, Svelte'ning class: direktivasi orqali. Aynan shu yerda eng katta tuzoq yashiringan.


22.4 ENG MUHIM TUZOQ β€” dinamik klass nomini "yig'ish"

Bu β€” Tailwind'da yangi kelgan har bir kishini kamida bir marta chalg'itadigan xato. Diqqat bilan o'qing.

Tailwind klasslarni qayerdan biladi? U sizning manba fayllaringizni o'qib (scan qilib), ichidagi to'liq klass nomlarini topadi va faqat o'shalar uchun CSS yaratadi. Demak skaner faqat matn sifatida ko'rgan klassni biladi.

Endi quyidagi "aqlli" kodga qarang:

// ❌ ISHLAMAYDI β€” skaner bu klassni hech qachon ko'rmaydi
function Badge({ color }) {
  return <span className={`text-${color}-500 bg-${color}-100`}>...</span>;
}

<Badge color="red" />   // text-red-500 kutasiz...

Mantiqan color="red" bo'lsa text-red-500 chiqishi kerakdek. Lekin chiqmaydi. Sabab: skaner faylda text-${color}-500 degan parchani ko'radi β€” bu to'liq klass emas, u text-red-500 degan satrni hech qachon topmaydi (u faqat ishlash vaqtida birikadi). Tailwind esa ishlash vaqtini ko'rmaydi β€” u faqat statik matnni ko'radi. Natijada text-red-500 CSS'i umuman generatsiya qilinmaydi, va badge rangsiz chiqadi.

πŸ“Œ Oltin qoida. Klass nomini hech qachon string birikmasidan yig'mang. Skaner to'liq klass nomini matn sifatida ko'ra olishi shart.

To'g'ri yechim β€” qidiruv obyekti (lookup map). To'liq klass nomlarini oldindan, statik holda yozib qo'yasiz, prop esa shulardan birini tanlaydi:

// βœ… TO'G'RI β€” to'liq klasslar statik matnda turibdi
const COLORS = {
  red:   "text-red-500 bg-red-100",
  green: "text-green-500 bg-green-100",
  blue:  "text-blue-500 bg-blue-100",
};

function Badge({ color = "blue" }) {
  return <span className={COLORS[color]}>...</span>;
}

Endi skaner faylda text-red-500, text-green-500, text-blue-500 ni to'liq matn sifatida ko'radi β€” hammasi generatsiya qilinadi. color propi esa shu tayyor variantlardan birini tanlaydi, klass yasamaydi.

πŸ’‘ Ag klass nomlari haqiqatan tashqi/dinamik manbadan kelsa (masalan CMS'dan) va ularni statik yoza olmasangiz, oxirgi chora β€” @source inline(...) bilan kerakli klasslarni qo'lda "ro'yxatdan o'tkazish". Buni 20-bobda ko'rgansiz. Lekin 99% holatda lookup obyekti to'g'ri va yetarli yechim.


22.5 clsx β€” klasslarni shartli birlashtirish

Komponentlar ko'pincha klasslarni shartga qarab qo'shadi: tugma faol bo'lsa boshqa rang, o'chiq bo'lsa xira. Sof JavaScript'da bu tez "ternar botqog'i"ga aylanadi:

// 😩 O'qish qiyin β€” ternar va string yopishtirish aralashmasi
className={"px-4 py-2 " + (isActive ? "bg-indigo-600 text-white " : "") + (isDisabled ? "opacity-50 " : "")}

clsx (kichik, mashhur kutubxona) aynan shuni tozalaydi. U argumentlarni qabul qiladi va faqat "rost" bo'lganlarini bo'sh joy bilan birlashtiradi:

import clsx from "clsx";

className={clsx(
  "px-4 py-2",                       // har doim
  isActive && "bg-indigo-600 text-white",   // faqat faol bo'lsa
  isDisabled && "opacity-50",        // faqat o'chiq bo'lsa
  { "ring-2 ring-indigo-400": isFocused }    // obyekt ko'rinishi ham mumkin
)}

false, undefined, null β€” hammasi e'tiborsiz tashlanadi. Natija β€” toza, o'qiladigan, shartli klass qatori. clsx ziddiyatni hal qilmaydi β€” u shunchaki rost bo'lganlarni qo'shadi. Ziddiyat β€” keyingi asbobning vazifasi.


22.6 tailwind-merge (twMerge) β€” ziddiyatni hal qilish

Mana nozik, lekin juda muhim muammo. Komponentingizda asosiy px-2 bor, lekin chaqiruvchi px-4 bilan ustiga yozmoqchi. Ikkalasini birlashtirsangiz, satrda ikkalasi ham qoladi:

<button class="px-2 py-1 px-4">...</button>

Endi qaysi biri yutadi? CSS'da bir xil spetsifiklikdagi qoidalar uchun manba tartibi hal qiladi β€” lekin bu Tailwind'ning generatsiya qilingan CSS faylidagi tartib, sizning satringizdagi tartib emas. Ya'ni natija aniqlanmagan (undefined) β€” px-2 ham, px-4 ham yutishi mumkin. Bu "override ishlamayapti" degan jumboqning eng keng tarqalgan sababi.

tailwind-merge (qisqacha twMerge) shu muammoni hal qiladi: u ziddiyatli Tailwind klasslarini taniydi va har bir xususiyat uchun faqat oxirgisini qoldiradi:

import { twMerge } from "tailwind-merge";

twMerge("px-2 px-4");               // β†’ "px-4"   (oxirgisi yutadi)
twMerge("px-2 py-1 bg-red-500 bg-blue-500");  // β†’ "px-2 py-1 bg-blue-500"
twMerge("text-sm text-lg");         // β†’ "text-lg"

Diqqat β€” twMerge faqat ziddiyatli (bir xil CSS xususiyatini boshqaradigan) klasslarni o'chiradi. px-2 va py-1 ziddiyatli emas (biri gorizontal, biri vertikal padding), shuning uchun ikkalasi ham qoladi. Bu β€” oddiy string yopishtirishdan tubdan farqi.

πŸ“Œ twMerge aynan className propini qabul qiladigan komponentlar uchun zarur. Propsiz oddiy komponentda ziddiyat bo'lmaydi, demak u kerak emas.


22.7 cn yordamchisi β€” clsx va twMerge ni birlashtirish

clsx shartlilarni birlashtiradi, twMerge ziddiyatni hal qiladi β€” ikkalasi ham kerak. Shuning uchun jamoalar ularni bitta kichik yordamchiga o'raydi. Bu naqshni shadcn/ui ommalashtirdi va u amalda standartga aylandi β€” odatda cn (yoki clsx) deb nomlanadi:

// lib/utils.js
import clsx from "clsx";
import { twMerge } from "tailwind-merge";

export function cn(...inputs) {
  return twMerge(clsx(inputs));
}

Ichkarida: avval clsx shartli argumentlarni bitta qatorga yig'adi, so'ng twMerge o'sha qatordagi ziddiyatlarni hal qiladi. Endi cn β€” bitta universal vosita: ham shartli, ham ziddiyatga chidamli.

Mana u className propini qabul qiladigan tugmada qanday ishlaydi:

import { cn } from "@/lib/utils";

function Button({ className, ...props }) {
  return (
    <button
      className={cn(
        "px-4 py-2 rounded-lg bg-indigo-600 text-white",  // asos
        className                                          // chaqiruvchining override'i
      )}
      {...props}
    />
  );
}

// Chaqiruvchi paddingni o'zgartiradi:
<Button className="px-8">Keng tugma</Button>

Bu yerda asos px-4, chaqiruvchi px-8 berdi. cn ichidagi twMerge ziddiyatni hal qiladi β€” natija px-8 (oxirgisi yutadi), px-4 esa tushib qoladi. Agar oddiy string yopishtirish ishlatganingizda, px-4 px-8 ikkalasi ham qolib, natija aniqlanmagan bo'lardi.

cn helper oqimi: asosiy klasslar va shartli klasslar clsx orqali birlashtiriladi, chaqiruvchidan kelgan override qo'shiladi, twMerge esa ziddiyatli klasslarni hal qiladi (px-2 ga qarshi px-4 dan px-4 yutadi) va yakuniy className hosil bo'ladi

πŸ’‘ Eslab qoling. className prop + cn = chaqiruvchi komponentni ishonchli sozlay oladi. className ni shunchaki yopishtirish ("asos " + className) esa override'ni jimgina ishlamay qoldiradi β€” bu eng ko'p uchraydigan "men override berdim, lekin o'zgarmadi" xatosi.


22.8 cva β€” variantli komponentlar

Tugmaning bir nechta ko'rinishi bo'ladi: asosiy (primary), ikkilamchi (secondary), xavfli (danger); kichik, o'rta, katta. Buni cn bilan qo'lda boshqarish (har bir kombinatsiya uchun if) tez chalkashadi. class-variance-authority (qisqacha cva) aynan shu uchun: siz variantlar jadvalini e'lon qilasiz, u esa props bo'yicha to'g'ri klasslarni qaytaradi.

import { cva } from "class-variance-authority";

const button = cva(
  // 1. ASOS β€” har doim qo'llanadigan klasslar
  "inline-flex items-center justify-center rounded-lg font-medium transition-colors",
  {
    // 2. VARIANTLAR
    variants: {
      intent: {
        primary:   "bg-indigo-600 text-white hover:bg-indigo-700",
        secondary: "bg-slate-200 text-slate-900 hover:bg-slate-300",
        danger:    "bg-red-500 text-white hover:bg-red-600",
      },
      size: {
        sm: "px-3 py-1.5 text-sm",
        md: "px-4 py-2 text-base",
        lg: "px-6 py-3 text-lg",
      },
    },
    // 3. STANDART β€” prop berilmasa shu ishlatiladi
    defaultVariants: {
      intent: "primary",
      size: "md",
    },
  }
);

Endi chaqirish β€” kerakli variantni nom bilan tanlaysiz, cva to'g'ri klasslarni yig'ib beradi:

button({ intent: "primary", size: "lg" });
// β†’ "inline-flex ... rounded-lg ... bg-indigo-600 text-white hover:bg-indigo-700 px-6 py-3 text-lg"

button({ intent: "danger" });          // size standart "md" bo'ladi
button();                               // ikkalasi ham standart: primary + md

cva bilan qurilgan Button: asosiy klasslar ustiga intent (primary/secondary/danger) va size (sm/md/lg) variantlari jadvali; button intent primary size lg chaqiruvi aniq klasslar to'plamini va render qilingan tugmani beradi

cva ning yana bir ustunligi β€” tiplangan (typed). TypeScript'da intent/size propslari avtomatik tiplanadi, noto'g'ri qiymat (intent: "purple") kompilyatsiya xatosini beradi. Variantlar ham 18-bobdagi dizayn tokenlariga tabiiy bog'lanadi: intent qiymatlari semantik tokenlarni (bg-primary, bg-danger) ishlatsa, tema o'zgarganda variantlar avtomatik moslashadi.

πŸ’‘ Muqobil β€” tailwind-variants (tv). cva ga juda o'xshash yana bir kutubxona bor: tailwind-variants. Uning ikkita qo'shimcha qulayligi: slotlar (bitta komponentning bir nechta qismini β€” masalan tugmaning ikonkasi va matni β€” alohida stillash) va ichiga qurilgan twMerge (ziddiyat avtomatik hal bo'ladi). Agar komponentlaringiz ko'p qismli bo'lsa, tv ni ko'rib chiqing; oddiy holatlar uchun cva yetarli.


22.9 Hammasini birlashtirish β€” production darajasidagi <Button>

Endi cva (variantlar) va cn (className override + ziddiyat) ni birlashtiramiz. Mana Tailwind ekotizimidagi kanonik, qayta ishlatiladigan tugma β€” shadcn/ui aynan shu naqshda qurilgan:

import { cva } from "class-variance-authority";
import { cn } from "@/lib/utils";

const buttonVariants = cva(
  "inline-flex items-center justify-center rounded-lg font-medium transition-colors " +
    "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-400 " +
    "disabled:opacity-50 disabled:pointer-events-none",
  {
    variants: {
      intent: {
        primary:   "bg-indigo-600 text-white hover:bg-indigo-700",
        secondary: "bg-slate-200 text-slate-900 hover:bg-slate-300",
        danger:    "bg-red-500 text-white hover:bg-red-600",
      },
      size: {
        sm: "px-3 py-1.5 text-sm",
        md: "px-4 py-2 text-base",
        lg: "px-6 py-3 text-lg",
      },
    },
    defaultVariants: { intent: "primary", size: "md" },
  }
);

export function Button({ intent, size, className, ...props }) {
  return (
    <button
      className={cn(buttonVariants({ intent, size }), className)}
      {...props}
    />
  );
}

className={cn(buttonVariants({ intent, size }), className)} β€” bu qatorda butun bobning mohiyati jamlangan:

  1. buttonVariants({ intent, size }) β€” cva props bo'yicha to'g'ri variant klasslarini beradi.
  2. cn(..., className) β€” twMerge orqali chaqiruvchining className override'i variant klasslari bilan ziddiyatsiz birlashadi (oxirgisi yutadi).

Foydalanish β€” toza va kuchli:

<Button>Saqlash</Button>                                  {/* primary + md */}
<Button intent="danger" size="sm">O'chirish</Button>
<Button intent="secondary" className="w-full">Bekor qilish</Button>  {/* to'liq kenglik override */}

Oxirgi misolda secondary variant w-full bilan kengaytirildi β€” className orqali, ziddiyatsiz. Bitta <Button> ta'rifi β€” cheksiz ko'rinish.


22.10 Qisqacha Vue misoli

Tushuncha framework'dan mustaqil. Vue'da cn/cva xuddi shunday ishlaydi, faqat :class bog'lamasi orqali ulaysiz:

<script setup>
import { computed } from "vue";
import { cva } from "class-variance-authority";
import { cn } from "@/lib/utils";

const props = defineProps({ intent: String, size: String, class: String });

const button = cva("inline-flex items-center rounded-lg font-medium transition-colors", {
  variants: {
    intent: { primary: "bg-indigo-600 text-white", secondary: "bg-slate-200 text-slate-900" },
    size: { sm: "px-3 py-1.5 text-sm", md: "px-4 py-2 text-base" },
  },
  defaultVariants: { intent: "primary", size: "md" },
});

const classes = computed(() =>
  cn(button({ intent: props.intent, size: props.size }), props.class)
);
</script>

<template>
  <button :class="classes"><slot /></button>
</template>

⚠️ Agar shu Vue komponentining <style scoped> blokida @apply yoki theme() ishlatsangiz, blok boshida @reference "../app.css"; yozishni unutmang β€” 20-bobda ko'rganimizdek, scoped blok Tailwind kontekstini avtomatik ko'rmaydi. Yuqoridagi misol esa faqat utility klasslar bilan ishlagani uchun bunga muhtoj emas.


22.11 Tez-tez uchraydigan xatolar

  • Dinamik klass nomini yig'ish β€” `text-${color}-500`. Skaner uni ko'rmaydi, klass generatsiya qilinmaydi. Lookup obyekti yoki @source inline(...) ishlating (22.4).
  • @apply ni ortiqcha ishlatish β€” o'z markup'ingizni boshqaradigan har bir komponentni CSS'da qayta qurish. Buning o'rniga komponent abstraksiyasi (22.1). @apply faqat global/uchinchi-tomon holatlari uchun.
  • twMerge siz className qabul qilish β€” "asos " + className yopishtirish override'ni jimgina ishlamay qoldiradi (px-4 px-8 ikkalasi qoladi, qaysi biri yutishi noaniq). Doim cn orqali birlashtiring (22.7).
  • @reference ni unutish β€” Vue/Svelte/Astro scoped <style> da @apply ishlatib, "nega ishlamayapti" deb hayron bo'lish. v4 da scoped blok @reference "../app.css"; talab qiladi (22.10, 20-bob).
  • clsx va twMerge ni adashtirish β€” clsx shartlilarni qo'shadi, ziddiyatni hal qilmaydi; twMerge ziddiyatni hal qiladi. Komponentga ikkalasi ham kerak β€” shuning uchun cn ularni birlashtiradi.
  • Variant uchun if/ternar botqog'i β€” har bir intent/size kombinatsiyasini qo'lda yozish. cva (yoki tv) buni jadvalga aylantiradi (22.8).

πŸ”­ Oldinga qarash. Endi sizda qayta ishlatiladigan komponent qurishning to'liq asboblar to'plami bor: komponent abstraksiyasi, clsx, twMerge, cn va cva. Keyingi bobda shu asboblarni ishga solib, haqiqiy UI komponentlari β€” formalar, inputlar, modallar va kartalar β€” quramiz. Bu bobning <Button>'i β€” o'sha amaliyotning birinchi g'ishti.


Mashqlar

1-mashq. Bir hamkasbingiz 12 ta mahsulot kartasini ko'rsatish uchun class="rounded-xl bg-white p-6 shadow-md" ni JSX'da 12 marta nusxalagan. Endi soyani shadow-lg ga o'zgartirish kerak. Nima uchun bu yondashuv muammoli, va to'g'ri yechim qanday? Qisqa kod bilan ko'rsating.

Yechim

Muammo: klass 12 ta manbada β€” bittasini o'zgartirsangiz, qolgan 11 tasini qo'lda topib tuzatishingiz kerak (xato kiritish ehtimoli yuqori, kod shishadi). To'g'ri yechim β€” komponent abstraksiyasi: klassni bir joyda saqlash.

function ProductCard({ children }) {
  return <div className="rounded-xl bg-white p-6 shadow-md">{children}</div>;
}

{products.map((p) => <ProductCard key={p.id}>{p.name}</ProductCard>)}

Endi shadow-md β†’ shadow-lg o'zgarishi bitta joyda qilinadi, 12 ta karta birga yangilanadi. @apply shart emas β€” klass JSX'da, ko-lokatsiya saqlanadi.

2-mashq. Quyidagi React kodi bg-green-100 ni hech qachon chiqarmaydi. Sababini ayting va tuzating.

function Tag({ tone }) {
  return <span className={`bg-${tone}-100 text-${tone}-700`}>{tone}</span>;
}
<Tag tone="green" />
Yechim

Sabab: klass nomi string birikmasidan ishlash vaqtida yig'iladi. Tailwind skaneri faylda faqat bg-${tone}-100 parchasini ko'radi β€” bg-green-100 degan to'liq matn hech qachon faylda yo'q, shuning uchun u CSS generatsiya qilmaydi.

Tuzatish β€” lookup obyekti (to'liq klasslar statik matnda):

const TONES = {
  green: "bg-green-100 text-green-700",
  red:   "bg-red-100 text-red-700",
  blue:  "bg-blue-100 text-blue-700",
};

function Tag({ tone = "blue" }) {
  return <span className={TONES[tone]}>{tone}</span>;
}

Endi skaner bg-green-100, bg-red-100, bg-blue-100 ni to'liq ko'radi va generatsiya qiladi. (Agar tonlar tashqi/dinamik manbadan kelsa β€” @source inline(...), 20-bob.)

3-mashq. clsx va twMerge orasidagi farqni bitta jumlada tushuntiring. Quyidagi har bir chaqiruv nima qaytaradi?

clsx("px-2", false && "hidden", "px-4")
twMerge("px-2 px-4")
Yechim

Farq: clsx faqat "rost" argumentlarni birlashtiradi (ziddiyatga qaramaydi); twMerge esa ziddiyatli Tailwind klasslarini hal qiladi (bir xil xususiyat uchun oxirgisini qoldiradi).

  • clsx("px-2", false && "hidden", "px-4") β†’ "px-2 px-4" β€” false tashlanadi, qolgan ikkalasi qoladi (ziddiyat hal qilinmaydi).
  • twMerge("px-2 px-4") β†’ "px-4" β€” ikkalasi ham horizontal paddingni boshqaradi, oxirgisi yutadi.

Shuning uchun komponentga ikkalasi ham kerak: cn = (...x) => twMerge(clsx(x)).

4-mashq. Quyidagi tugma komponentida chaqiruvchi className="bg-red-500" bersa ham, fon ko'k (bg-indigo-600) bo'lib qolyapti β€” yoki natija oldindan aytib bo'lmaydigan. Sababini toping va tuzating.

function Button({ className, ...props }) {
  return <button className={"bg-indigo-600 text-white px-4 py-2 " + className} {...props} />;
}
<Button className="bg-red-500">O'chirish</Button>
Yechim

Sabab: klasslar oddiy string yopishtirish bilan birlashtirilgan, shuning uchun natija "bg-indigo-600 ... bg-red-500" β€” ikkala fon klassi ham satrda qoladi. Qaysi biri yutishi sizning satringizdagi tartibga emas, Tailwind generatsiya qilgan CSS faylidagi tartibga bog'liq, ya'ni override ishonchsiz.

Tuzatish β€” cn (twMerge ichida) ishlatish:

import { cn } from "@/lib/utils";

function Button({ className, ...props }) {
  return <button className={cn("bg-indigo-600 text-white px-4 py-2", className)} {...props} />;
}

Endi twMerge bg-indigo-600 va bg-red-500 ziddiyatini taniydi, oxirgisini (bg-red-500) qoldiradi β€” override ishonchli ishlaydi.

5-mashq. cva bilan uchta intent (primary/secondary/ghost) va ikkita size (sm/lg) ga ega badge ta'rifini yozing; standart β€” primary + sm. So'ng badge({ intent: "ghost", size: "lg" }) chaqiruvini va badge() (standart) ni ko'rsating.

Yechim
import { cva } from "class-variance-authority";

const badge = cva("inline-flex items-center rounded-full font-medium", {
  variants: {
    intent: {
      primary:   "bg-indigo-600 text-white",
      secondary: "bg-slate-200 text-slate-800",
      ghost:     "bg-transparent text-slate-600 border border-slate-300",
    },
    size: {
      sm: "px-2.5 py-0.5 text-xs",
      lg: "px-4 py-1.5 text-sm",
    },
  },
  defaultVariants: { intent: "primary", size: "sm" },
});

badge({ intent: "ghost", size: "lg" });
// β†’ "inline-flex items-center rounded-full font-medium bg-transparent text-slate-600 border border-slate-300 px-4 py-1.5 text-sm"

badge();
// β†’ "inline-flex items-center rounded-full font-medium bg-indigo-600 text-white px-2.5 py-0.5 text-xs"  (standart: primary + sm)

6-mashq. Loyihangizda Markdown'dan render bo'ladigan blog matni bor. Ichidagi barcha <blockquote> larga chap chegara va xira matn berishingiz kerak β€” lekin Markdown generatsiya qilgan HTML'ga klass qo'sha olmaysiz. @apply bu yerda o'rinli holatga misolmi? Kod yozing va nega komponent abstraksiyasi bu yerda ishlamasligini ayting.

Yechim

Ha β€” bu @apply o'rinli bo'lgan tipik holat: siz markup'ni boshqarmaysiz (u Markdown'dan keladi), shuning uchun komponent abstraksiyasi yoki className qo'shish mumkin emas. Yagona yo'l β€” selektor orqali yetib borish:

@layer components {
  .prose blockquote {
    @apply border-l-4 border-slate-300 pl-4 text-slate-600 italic;
  }
}

Nega komponent ishlamaydi: komponent abstraksiyasi siz <Quote> yozib, unga className qo'ya olganingda ishlaydi. Bu yerda HTML'ni Markdown protsessori chiqaradi β€” sizning qo'lingizda <blockquote> ga klass yozish imkoni yo'q. Demak @apply + selektor β€” to'g'ri tanlov.

Eslatma: agar bu CSS Vue/Svelte scoped <style> blokida bo'lsa, boshida @reference "../app.css"; kerak bo'ladi (20-bob).


⬅️ Oldingi: 21 β€” Plaginlar ekotizimi Β· 🏠 README Β· Keyingi: 23 β€” Forma va UI komponentlari amaliy ➑️