eduardweb.
Hooks & PatternsAvansat#typescript#react#frontend#react-patterns

Pattern-uri de compound components: Cum scrii un <Accordion> flexibil și curat

De Teodor Pascu, 12 iul. 2026 · 13 vizualizări · 3 like-uri

Postat 12 iul. 2026
typescript
import React, { createContext, useContext, useState } from "react";

const AccordionContext = createContext<{ 
  openId: string | null; 
  toggle: (id: string) => void; 
} | null>(null);

export function Accordion({ children }: { children: React.ReactNode }) {
  const [openId, setOpenId] = useState<string | null>(null);
  const toggle = (id: string) => setOpenId(openId === id ? null : id);

  return (
    <AccordionContext.Provider value={{ openId, toggle }}>
      <div className="border rounded-md divide-y">{children}</div>
    </AccordionContext.Provider>
  );
}

const ItemContext = createContext<string | null>(null);

export function AccordionItem({ id, children }: { id: string; children: React.ReactNode }) {
  return (
    <ItemContext.Provider value={id}>
      <div className="p-2">{children}</div>
    </ItemContext.Provider>
  );
}

export function AccordionTrigger({ children }: { children: React.ReactNode }) {
  const group = useContext(AccordionContext);
  const itemId = useContext(ItemContext);
  
  if (!group || !itemId) {
    throw new Error("AccordionTrigger trebuie folosit în interiorul AccordionItem");
  }

  return (
    <button 
      onClick={() => group.toggle(itemId)} 
      className="w-full text-left font-medium py-2 flex justify-between"
    >
      {children}
    </button>
  );
}

export function AccordionContent({ children }: { children: React.ReactNode }) {
  const group = useContext(AccordionContext);
  const itemId = useContext(ItemContext);

  if (!group || !itemId) {
    throw new Error("AccordionContent trebuie folosit în interiorul AccordionItem");
  }

  return group.openId === itemId ? (
    <div className="py-2 text-gray-600 transition-all">{children}</div>
  ) : null;
}

Am pățit-o cu toții. Scrii o componentă de Accordion, o pui în producție, iar a doua zi colegul de la marketing îți cere să pui o iconiță în stânga titlului, doar pe o pagină anume. Săptămâna următoare, altcineva vrea un badge lângă text. Te trezești rapid cu un monstru plin de prop-uri gen iconPosition, showBadge, badgeText sau customHeaderClassName. Codul devine greu de citit, rigid și imposibil de întreținut.

Soluția nu e să tot adaugi prop-uri până când componenta crapă sub propria greutate. Soluția este să delegi controlul structurii către cel care o folosește, fix cum fac bibliotecile moderne gen Radix UI sau shadcn/ui.

Cum am salvat 40% din codul de UI

La un proiect anterior, o platformă SaaS cu peste 15.000 de utilizatori activi, aveam opt variante diferite de liste colapsabile. Fiecare echipă își scrisese propria versiune pentru că cea originală nu era destul de flexibilă. Când am refăcut UI-ul folosind pattern-ul de compound components, am șters aproape 40% din codul duplicat.

Ideea de bază e simplă: în loc să ai o singură componentă gigant, spargi totul în sub-componente care comunică între ele în spate printr-un context React partajat. Consumatorul asamblează aceste piese exact cum are nevoie.

Anatomia unui Accordion flexibil

Secretul stă în modul în care definim contextul. Fiecare AccordionItem trebuie să știe dacă este deschis sau închis, iar AccordionTrigger trebuie să poată schimba această stare fără ca tu să pasezi manual funcții de toggle prin tot arborele de componente.

Uită-te la exemplul de cod atașat. Observi cum consumatorul are control total asupra markup-ului? Dacă vrei să pui un div suplimentar sau un stil custom pe header, pur și simplu îl scrii acolo. Componenta nu mai dictează structura HTML, ci doar comportamentul și accesibilitatea (WAI-ARIA).

Trade-off-ul sincer pe care trebuie să-l accepți

Nimic nu e gratis în ingineria software, iar compound components vin cu propriile bătăi de cap:

  1. Zgomot în cod: În loc de o singură linie <Accordion data={items} />, acum ai nevoie de 10-15 linii de cod pentru a randa aceeași chestie. Pentru pagini simple, poate părea un boilerplate inutil.
  2. Erori tăcute la runtime: Dacă un coleg pune un AccordionTrigger în afara unui AccordionItem, TypeScript s-ar putea să nu urle, dar în browser vei avea un crash de tipul Cannot read properties of undefined când încerci să citești contextul. De aceea e vital să arunci o eroare explicită în hook-ul personalizat, așa cum am făcut în exemplul de cod cu useAccordionContext.
  3. Performanța: Dacă ai sute de iteme și schimbi starea des, contextul va randa din nou copiii. Totuși, pentru un accordion normal de 5-10 elemente, impactul e practic zero.

În final, merită efortul? Absolut. Câștigi o flexibilitate uriașă și nu mai trebuie să modifici componenta de bază de fiecare dată când designerul are o idee nouă la cafea.

Cum gestionați voi componentele complexe în proiectele voastre? Mergeți direct pe compound components sau doar când varianta cu prop-uri devine de necontrolat?

Răspunsuri 0

Se încarcă răspunsurile…

Loghează-te pentru a răspunde

Doar membrii comunității pot lăsa comentarii.