Compound Component Pattern
React-н component-ууд томрох тусам нэг component-д хэт олон props нэмэгдэж, ашиглахад төвөгтэй болдог. Compound Component Pattern нь энэ асуудлыг шийддэг — нэг том component-ыг хэд хэдэн жижиг, холбогдох хэсгүүдэд хувааж, HTML-н <select> / <option> шиг хамтдаа ажиллуулах арга. Эхэндээ нарийн мэт санагдах ч нэг удаа ойлгосны дараа маш хүчирхэг хэрэгсэл болдог.
Асуудал: Props тэсрэлт
<Modal> component-д бүх зүйлийг props-оор дамжуулах гэвэл:
// ❌ Prop тэсрэлт — ашиглахад хэцүү
<Modal
title="Баталгаажуулах"
description="Та энэ хичээлийг устгахдаа итгэлтэй байна уу?"
confirmLabel="Устгах"
cancelLabel="Болих"
confirmVariant="danger"
showCloseButton={true}
headerIcon="warning"
footerAlign="right"
onConfirm={handleDelete}
onCancel={handleClose}
onClose={handleClose}
/>
Нэг харахад хэдэн props байгааг тоолох хэрэгтэй болно. Compound Pattern ашиглавал:
// ✅ Compound Pattern — уншихад хялбар, уян хатан
<Modal>
<Modal.Header icon="warning">Баталгаажуулах</Modal.Header>
<Modal.Body>Та энэ хичээлийг устгахдаа итгэлтэй байна уу?</Modal.Body>
<Modal.Footer align="right">
<Modal.CancelButton onClick={handleClose}>Болих</Modal.CancelButton>
<Modal.ConfirmButton variant="danger" onClick={handleDelete}>
Устгах
</Modal.ConfirmButton>
</Modal.Footer>
</Modal>
Context ашиглан Compound Component бүтээх
Compound Component-ийн гол санаа нь parent-аас child-уудад Context-ээр мэдээлэл дамжуулна:
// src/components/Tabs/Tabs.tsx
import { createContext, useContext, useState, ReactNode } from "react";
interface TabsContextValue {
activeTab: string;
setActiveTab: (tab: string) => void;
}
const TabsContext = createContext<TabsContextValue | null>(null);
function useTabs() {
const ctx = useContext(TabsContext);
if (!ctx) throw new Error("useTabs нь <Tabs> дотор ашиглагдах ёстой");
return ctx;
}
// Үндсэн Tabs component — Context provider
interface TabsProps {
defaultTab: string;
children: ReactNode;
}
function Tabs({ defaultTab, children }: TabsProps) {
const [activeTab, setActiveTab] = useState(defaultTab);
return (
<TabsContext.Provider value={{ activeTab, setActiveTab }}>
<div className="tabs">{children}</div>
</TabsContext.Provider>
);
}
// Tab.List — tab товчнуудын container
function TabList({ children }: { children: ReactNode }) {
return (
<div className="tab-list" role="tablist">
{children}
</div>
);
}
// Tab.Tab — нэг tab товч
interface TabProps {
value: string;
children: ReactNode;
}
function Tab({ value, children }: TabProps) {
const { activeTab, setActiveTab } = useTabs();
const isActive = activeTab === value;
return (
<button
role="tab"
aria-selected={isActive}
className={isActive ? "tab tab--active" : "tab"}
onClick={() => setActiveTab(value)}
>
{children}
</button>
);
}
// Tab.Panel — tab-н агуулга
interface TabPanelProps {
value: string;
children: ReactNode;
}
function TabPanel({ value, children }: TabPanelProps) {
const { activeTab } = useTabs();
if (activeTab !== value) return null;
return (
<div role="tabpanel" className="tab-panel">
{children}
</div>
);
}
// Дэд component-уудыг Tabs-д холбох
Tabs.List = TabList;
Tabs.Tab = Tab;
Tabs.Panel = TabPanel;
export default Tabs;
Compound Component ашиглах
Бүтээсэн Tabs-ыг дараах байдлаар ашиглана:
import Tabs from "./components/Tabs/Tabs";
function CourseDetail() {
return (
<Tabs defaultTab="хичээлүүд">
<Tabs.List>
<Tabs.Tab value="хичээлүүд">Хичээлүүд</Tabs.Tab>
<Tabs.Tab value="төслүүд">Төслүүд</Tabs.Tab>
<Tabs.Tab value="мэдээлэл">Мэдээлэл</Tabs.Tab>
</Tabs.List>
<Tabs.Panel value="хичээлүүд">
<LessonList />
</Tabs.Panel>
<Tabs.Panel value="төслүүд">
<ProjectList />
</Tabs.Panel>
<Tabs.Panel value="мэдээлэл">
<CourseInfo />
</Tabs.Panel>
</Tabs>
);
}
Tabs.List, Tabs.Tab, Tabs.Panel гэж namespace-тэй байгааг анзаарна уу — кодыг уншихад бүгд нэг гэр бүлд хамаарч байгааг тэр дор харж болно.
Accordion Compound Component
Өөр нэг нийтлэг жишээ — accordion:
// src/components/Accordion/Accordion.tsx
import { createContext, useContext, useState, ReactNode } from "react";
interface AccordionContextValue {
openItem: string | null;
toggle: (item: string) => void;
}
const AccordionContext = createContext<AccordionContextValue | null>(null);
function useAccordion() {
const ctx = useContext(AccordionContext);
if (!ctx)
throw new Error("useAccordion нь <Accordion> дотор ашиглагдах ёстой");
return ctx;
}
function Accordion({ children }: { children: ReactNode }) {
const [openItem, setOpenItem] = useState<string | null>(null);
const toggle = (item: string) => {
setOpenItem((prev) => (prev === item ? null : item));
};
return (
<AccordionContext.Provider value={{ openItem, toggle }}>
<div className="accordion">{children}</div>
</AccordionContext.Provider>
);
}
interface ItemProps {
value: string;
children: ReactNode;
}
function AccordionItem({ value, children }: ItemProps) {
const { openItem, toggle } = useAccordion();
const isOpen = openItem === value;
return (
<div className="accordion-item">
<button
className="accordion-trigger"
aria-expanded={isOpen}
onClick={() => toggle(value)}
>
{value}
<span>{isOpen ? "▲" : "▼"}</span>
</button>
{isOpen && <div className="accordion-content">{children}</div>}
</div>
);
}
Accordion.Item = AccordionItem;
export default Accordion;
Ашиглах жишээ:
<Accordion>
<Accordion.Item value="JavaScript гэж юу вэ?">
JavaScript бол вэб хөгжүүлэлтийн үндсэн хэл...
</Accordion.Item>
<Accordion.Item value="React яагаад хэрэгтэй вэ?">
React нь UI-г component болгон хуваадаг...
</Accordion.Item>
<Accordion.Item value="TypeScript-ийн давуу тал">
Алдааг бичиж байхдаа илрүүлдэг...
</Accordion.Item>
</Accordion>
Compound Component Pattern нь дизайн системд хамгийн их хэрэглэгддэг хэв маягуудын нэг. Tabs, Accordion, Dropdown, Modal, Form зэрэг ихэнх UI компонентийг энэ аргаар бүтээдэг. Нэг удаа бүтээгдсэн бол олон газар дахин ашиглаж болдог нь хамгийн том давуу тал.
Дараагийн хичээлд:
Render Props Pattern — component-ыг аль болох уян хатан болгохын тулд "юу render хийхийг" props-оор дамжуулах аргыг практикаар судална.