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

Compound Components în React: Cum am rescris un Accordion să nu mai plângă echipa

De Bogdan Răducanu, 14 oct. 2026 · 4 vizualizări · 2 like-uri

Postat acum 2 zile
typescript
import React, { createContext, useContext, useId } from "react";

interface ItemContextType {
  value: string;
  triggerId: string;
  contentId: string;
}

const AccordionItemCtx = createContext<ItemContextType | null>(null);

export const AccordionItem = ({ value, children }: { value: string; children: React.ReactNode }) => {
  const id = useId();
  const triggerId = `accordion-trigger-${id}`;
  const contentId = `accordion-content-${id}`;

  return (
    <AccordionItemCtx.Provider value={{ value, triggerId, contentId }}>
      <div className="border-b border-neutral-200 dark:border-neutral-800">
        {children}
      </div>
    </AccordionItemCtx.Provider>
  );
};

export const useAccordionItem = () => {
  const ctx = useContext(AccordionItemCtx);
  if (!ctx) {
    throw new Error("Componentele AccordionTrigger/Content trebuie împachetate într-un <AccordionItem>");
  }
  return ctx;
};

Dacă ai făcut vreodată un <Accordion items={[{ title, content }]} />, știi exact cât de repede devine un coșmar când designerul cere un badge lângă titlu sau un buton de acțiune în interior. Am rescris componenta asta de vreo patru ori în ultimii ani pe un dashboard cu 40k utilizatori activi lunar, până am trecut complet pe arhitectura de compound components. Azi îți arăt cum o construiești curat, inspirat din ce găsești în Radix UI și shadcn/ui.

Capcana API-ului bazat pe props monolitice

La început pare simplu: arunci un array de obiecte, tragi un .map() și gata treaba. Dar după două săptămâni vine cineva din echipă și zice că vrea un icon de status doar la al treilea rând, altcineva vrea ca deschiderea să fie animată cu Framer Motion, iar PM-ul vrea tracking de analytics pe fiecare click separat.

Ajungi instant la un API grotesc de tipul renderHeader, rightSlot, headerClassName și alte 15 props opționale. Am pierdut lejer vreo 18 ore de refactoring acum doi ani încercând să descurc un monstru din ăsta care adunase 20 de proprietăți în interfața de TypeScript.

Compound components inversează complet responsabilitatea. În loc să bagi date oarbe într-o cutie neagră, expui bucăți mici și autonome: Accordion, AccordionItem, AccordionTrigger și AccordionContent.

Cum legăm bucățile cu React Context

Secretul stă în două contexte suprapuse. Ai nevoie de un context rădăcină (care ține minte starea globală — ce panou e deschis) și un context individual per AccordionItem.

De ce per item? Pentru ca AccordionTrigger și AccordionContent să știe automat la ce valoare răspund, fără să pasezi manual ID-uri prin props de fiecare dată când le scrii în JSX. În plus, legătura asta ascunsă ne permite să generăm automat atributele WAI-ARIA (aria-controls, aria-expanded și id-uri sincronizate prin hook-ul useId).

Folosesc mereu un custom hook intern cu verificare strictă. Dacă un coleg junior mută accidental un AccordionTrigger în afara unui AccordionItem, arunc o eroare descriptivă direct în consolă în mod dev, în loc să crap silentios la runtime.

Trade-off-uri sincere: Când e overkill?

Pattern-ul ăsta nu e glonțul de argint pentru orice scenariu.

Merge brici pentru design systems interne consumate de 3-4 echipe diferite, unde fiecare are nevoie de libertate totală de styling și compoziție. Ai slot-uri libere peste tot, pui ce vrei între elemente și accesibilitatea rămâne intactă.

E complet nasol însă dacă ai doar un FAQ static pe un landing page unde textele se schimbă o dată pe an. Scrii de trei ori mai mult boilerplate, consumi memorie cu mai multe instanțe de context și complici codebase-ul degeaba. Pentru 5 întrebări și răspunsuri fixe, un array mapat dintr-un fișier JSON rămâne mult mai sănătos la cap.

Voi mai folosiți array-uri masive de configurare la componentele interactive sau ați trecut definitiv pe compoziție declarativă?

Răspunsuri 0

Se încarcă răspunsurile…

Loghează-te pentru a răspunde

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