import React, { createContext, useContext, useState } from 'react';
const AccordionContext = createContext<{
activeValue: string | null;
setActiveValue: (val: string | null) => void;
} | null>(null);
export function Accordion({ children, defaultValue = null }: { children: React.ReactNode; defaultValue?: string | null }) {
const [activeValue, setActiveValue] = useState<string | null>(defaultValue);
return (
<AccordionContext.Provider value={{ activeValue, setActiveValue }}>
<div className="border rounded divide-y">{children}</div>
</AccordionContext.Provider>
);
}
const ItemContext = createContext<string | null>(null);
export function AccordionItem({ value, children }: { value: string; children: React.ReactNode }) {
return (
<ItemContext.Provider value={value}>
<div className="p-2">{children}</div>
</ItemContext.Provider>
);
}
export function AccordionTrigger({ children }: { children: React.ReactNode }) {
const group = useContext(AccordionContext);
const value = useContext(ItemContext);
if (!group || !value) throw new Error('AccordionTrigger must be used within AccordionItem');
const isOpen = group.activeValue === value;
return (
<button
className="w-full text-left font-bold py-2 flex justify-between"
onClick={() => group.setActiveValue(isOpen ? null : value)}
>
{children}
<span>{isOpen ? '▲' : '▼'}</span>
</button>
);
}
export function AccordionContent({ children }: { children: React.ReactNode }) {
const group = useContext(AccordionContext);
const value = useContext(ItemContext);
if (!group || !value) throw new Error('AccordionContent must be used within AccordionItem');
return group.activeValue === value ? <div className="py-2 text-gray-600">{children}</div> : null;
}M-am lovit recent de o problemă destul de enervantă pe un proiect cu peste 12k utilizatori activi: aveam un sistem de design intern unde componentele de UI deveniseră niște monștri plini de prop-uri. Dacă voiai să adaugi o simplă iconiță sau o clasă de CSS custom în header-ul unui acordion, trebuia să pasezi ceva de genul renderHeaderIcon={() => <Icon />}. O mizerie greu de întreținut și imposibil de scalat.
Trecerea la compound components, în stilul celor din Radix UI sau shadcn, ne-a salvat sănătatea mintală și a redus timpul de dezvoltare cu aproape 30% la ecranele complexe.
De ce eșuează componentele "monolit"
Când construiești un <Accordion data={items} /> clasic, ascunzi toată structura DOM-ului în interiorul unei singure componente. Pare o idee bună la început pentru că ascunzi complexitatea. Dar imediat ce designerii vin cu excepții — de exemplu, când vor ca un singur item să aibă un background roșu, sau un buton de share chiar lângă trigger — ești blocat.
Începi să adaugi prop-uri opționale în disperare: itemClassName, triggerClassName, isCollapsible, customIcon. În scurt timp, ajungi la o componentă cu 25 de prop-uri pe care nimeni nu o mai înțelege și care crapă la fiecare update minor.
Soluția: Împărțirea responsabilităților prin Context
Ideea de bază din spatele compound components este simplă: în loc de o singură componentă mamut, expui un set de sub-componente care colaborează între ele folosind React Context. Logica de stare (care element este deschis) rămâne ascunsă, dar layout-ul și markup-ul sunt lăsate în totalitate în mâna developerului care folosește componenta.
Practic, <Accordion> devine doar un wrapper care oferă contextul. Sub-componentele precum <AccordionItem>, <AccordionTrigger> și <AccordionContent> consumă acest context în mod transparent. Ele pot fi așezate oricum în pagină, adăugându-le clase de Tailwind sau alte elemente HTML fără ca logica internă să fie afectată.
Trade-off-uri pe care trebuie să le accepți
Deși API-ul final este incredibil de curat, acest pattern vine cu un cost pe care trebuie să-l pui în balanță.
În primul rând, ai mult mai mult boilerplate de scris în biblioteca de componente. Trebuie să definești multiple contexte și să pui protecții la randare. Eu folosesc mereu un helper care aruncă o eroare dacă o sub-componentă e randată în afara providerului. Fără asta, un coleg s-ar putea trezi cu bug-uri tăcute și greu de debugat.
În al doilea rând, performanța de randare. Dacă ai un accordion masiv cu sute de elemente și schimbi starea, React va re-randa toate sub-componentele abonate la context. Pentru majoritatea cazurilor de UI (faq, sidebar, setări) nu contează absolut deloc, dar pentru tabele masive dinamice s-ar putea să simți un mic lag.
Pentru noi, flexibilitatea codului a bătut acele câteva milisecunde teoretice de render. Voi cum abordați componentele de UI complexe în proiectele voastre? Mergeți pe flexibilitatea compound components sau preferați rigiditatea sigură a prop-urilor standard?