import React, { createContext, useContext, useState } from 'react';
const AccordionContext = createContext<{
activeId: string | null;
toggle: (id: string) => void;
} | null>(null);
const AccordionItemContext = createContext<string | null>(null);
export function Accordion({ children }: { children: React.ReactNode }) {
const [activeId, setActiveId] = useState<string | null>(null);
const toggle = (id: string) => setActiveId(activeId === id ? null : id);
return (
<AccordionContext.Provider value={{ activeId, toggle }}>
<div className="border border-slate-200 rounded-lg">{children}</div>
</AccordionContext.Provider>
);
}
export function AccordionItem({ id, children }: { id: string; children: React.ReactNode }) {
return (
<AccordionItemContext.Provider value={id}>
<div className="border-b last:border-b-0 border-slate-200">{children}</div>
</AccordionItemContext.Provider>
);
}
export function AccordionTrigger({ children }: { children: React.ReactNode }) {
const globalContext = useContext(AccordionContext);
const itemId = useContext(AccordionItemContext);
if (!globalContext || !itemId) {
throw new Error('AccordionTrigger must be used inside AccordionItem');
}
const isOpen = globalContext.activeId === itemId;
return (
<button
onClick={() => globalContext.toggle(itemId)}
className="w-full flex justify-between p-4 font-medium text-left"
>
{children}
<span>{isOpen ? '▲' : '▼'}</span>
</button>
);
}
export function AccordionContent({ children }: { children: React.ReactNode }) {
const globalContext = useContext(AccordionContext);
const itemId = useContext(AccordionItemContext);
if (!globalContext || !itemId) {
throw new Error('AccordionContent must be used inside AccordionItem');
}
const isOpen = globalContext.activeId === itemId;
if (!isOpen) return null;
return <div className="p-4 pt-0 text-slate-600">{children}</div>;
}Am rescris anul trecut sistemul de design pentru un SaaS în zona de HR, cu vreo 14.000 de utilizatori activi. Codul vechi era plin de componente rigide. Cel mai bun exemplu? Un Accordion care primea un array uriaș de obiecte prin prop-ul items. Când designerul a vrut să adauge un badge de status doar lângă titlul celui de-al treilea rând, totul s-a blocat. Am ajuns să adăugăm prop-uri absurde ca renderHeader, titleClassName sau badgeColor în componenta principală.
Acolo am decis să trecem pe compound components. Este exact abordarea pe care o vezi în Radix UI sau Shadcn. În loc să ascunzi structura DOM în interiorul componentei, o lași la vedere și pasezi controlul către consumator.
Problema cu configurațiile prin props
Când scrii o componentă configurabilă exclusiv prin props, încerci practic să anticipezi viitorul. Spoiler: nu o să-ți iasă. Întotdeauna va apărea un caz special pe care nu l-ai prevăzut la început.
Dacă vrei să schimbi tag-ul h3 al titlului în h4 pentru SEO, trebuie să modifici componenta internă sau să pasezi un alt prop (titleAs="h4"). Cu pattern-ul compound, consumatorul componentei decide exact structura HTML, poziționarea iconițelor și stilizarea fiecărui element în parte.
Cum implementăm asta curat cu React Context
Ideea de bază e simplă: avem un context părinte care ține starea (care item e deschis) și sub-componente care consumă acest context. Pentru a face totul modular, folosim două niveluri de context. Primul este în Accordion (pentru starea globală de expandare), iar al doilea este în AccordionItem (pentru ca trigger-ul și conținutul să știe din ce secțiune fac parte, fără să le pasăm manual un ID prin props).
Pentru a evita erorile silențioase la runtime, eu folosesc mereu un custom hook care aruncă o eroare explicită dacă contextul este undefined. Asta te salvează de ore întregi de debugging când un coleg junior pune accidental <AccordionTrigger> în afara unui <AccordionItem>.
Trade-off-uri pe care trebuie să le accepți
Nimic nu e gratis în ingineria software, iar acest pattern vine cu un cost destul de clar.
Avantaje:
- Flexibilitate extremă: Poți pune absolut orice markup în interior, poți adăuga animații custom pe trigger-e specifice sau clase de Tailwind unde vrei tu.
- Separarea responsabilităților: Fiecare sub-componentă se ocupă de o singură bucată din DOM și de comportamentul ei specific.
Dezavantaje:
- Boilerplate crescut: În loc de o singură linie de cod unde pasai un array, acum scrii 10-15 linii pentru un simplu acordeon.
- Mentenanță mai grea pentru cazuri simple: Dacă ai nevoie de 50 de acordeoane identice în aplicație, scrierea repetitivă a aceleiași structuri devine obositoare. (Sfat: poți oricând să creezi o componentă wrapper simplă peste cea compound pentru cazurile repetitive).
Dacă scrii o librărie de componente internă sau lucrezi la un proiect mare care va evolua în timp, mergi pe compound components. Dacă ai de făcut un landing page rapid unde acordeonul e fix și nu se va schimba niciodată, o componentă clasică bazată pe props e suficientă.
Voi ce abordare folosiți în proiectele voastre? Preferiți flexibilitatea granulară stil Radix sau simplitatea unei componente gata configurate?