import React, { createContext, useContext, useState } from 'react';
type AccordionContextType = {
value: string | null;
toggle: (val: string) => void;
};
const AccordionContext = createContext<AccordionContextType | null>(null);
const ItemContext = createContext<{ value: string } | null>(null);
export const Accordion = ({ children }: { children: React.ReactNode }) => {
const [value, setValue] = useState<string | null>(null);
const toggle = (val: string) => setValue((prev) => (prev === val ? null : val));
return (
<AccordionContext.Provider value={{ value, toggle }}>
<div className="border rounded-md divide-y">{children}</div>
</AccordionContext.Provider>
);
};
export const AccordionItem = ({ value, children }: { value: string; children: React.ReactNode }) => (
<ItemContext.Provider value={{ value }}>
<div className="group">{children}</div>
</ItemContext.Provider>
);
export const AccordionTrigger = ({ children }: { children: React.ReactNode }) => {
const acc = useContext(AccordionContext);
const item = useContext(ItemContext);
if (!acc || !item) throw new Error('AccordionTrigger se folosește doar în AccordionItem');
const isOpen = acc.value === item.value;
return (
<button onClick={() => acc.toggle(item.value)} className="w-full flex justify-between p-4 font-medium">
{children}
<span>{isOpen ? '−' : '+'}</span>
</button>
);
};De ce ne încurcăm în prop-uri?
M-am lovit de zeci de ori în proiecte de clasicul <Accordion data={items} renderItem={...} allowMultiple={true} />. La început arată curat în JSX. Dar după două sprinturi, designerul vrea un icon diferit pe un singur item, un badge lângă titlu doar dacă e o alertă, și un dropdown de acțiuni în dreapta header-ului.
Rezultatul? Adaugi itemHeaderRightSlot, customIconFn, badgeCondition și alte 10 prop-uri. Componenta devine un monstru imposibil de întreținut.
Anul trecut, când am refăcut o secție critică din aplicație la un proiect cu peste 14k utilizatori activi, am eliminat complet abordarea asta monolitică în favoarea pattern-ului de Compound Components. Am rescris kit-ul de UI intern și am redus codul duplicat din pagini cu aproape 25%, având în același timp libertate totală pe partea de markup.
Anatomia unui Compound Component cu Context
Ideea din spatele shadcn/ui sau Radix UI nu e vreo magie, ci pură compoziție. Împarți componenta în bucăți atomice care comunică între ele printr-un React Context ascuns.
Avem nevoie de:
Accordion(root-ul care ține starea globală de deschidere)AccordionItem(context local pentru un element specific, ținevalue-ul lui)AccordionTrigger(butonul care comută starea)AccordionContent(panoul care se afișează sau ascunde)
Fără prop-drilling și fără să forțezi o structură rigidă în DOM. Dacă vrei să pui un <div> decorativ sau o linie de separare între Trigger și Content, poți să o faci fără să spargi logica.
Trucul din spate: Custom Hooks cu verificare de Context
Cea mai frecventă greșeală pe care o văd când devii implementează pattern-ul ăsta e că uită să protejeze contextul. Dacă cineva folosește <AccordionTrigger /> în afara unui <AccordionItem />, aplicația crapă silențios sau dă erori ciudate de React.
Secretul e un hook intern simplu care aruncă o eroare explicită în dev mode dacă contextul e null:
const useAccordionItemContext = () => {
const context = useContext(AccordionItemContext);
if (!context) {
throw new Error('AccordionTrigger/Content trebuie folosit în interiorul unui AccordionItem');
}
return context;
};
Asta salvează ore bune de debugging colegilor din echipă care folosesc componenta fără să-i știe neapărat arhitectura internă.
Trade-off-uri sincere: Când merită și când nu?
Nu totul e lapte și miere. Există un motiv pentru care nu orice componentă trebuie scrisă așa.
Când e perfect:
- Design systems, UI kit-uri interne, componente reutilizabile în tot proiectul.
- Când ai nevoie de acces la slot-uri flexibile (iconițe, badge-uri, butoane în header).
- Când vrei un DX impecabil în JSX.
Când e overkill:
- Pentru un widget banal de FAQ unde conținutul e 100% static și nu se va schimba niciodată structura visuală.
- Dacă echipa ta e la început cu React; JSX-ul mai verbos poate fi intimidant pentru cineva obișnuit doar să paseze un array de obiecte.
Marele câștig e că decuplezi logica de stare de structura vizuală. Voi ce folosiți în producție: primitive gen Radix/shadcn direct sau vă scrieți propriile compoziții?