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

Pattern-ul Compound Components în React: Cum construiești un Accordion flexibil stil shadcn

De Maria Vasilescu, 21 iul. 2026 · 10 vizualizări · 3 like-uri

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

type AccordionContextType = {
  openValue: string | null;
  toggle: (val: string) => void;
};

const AccordionContext = createContext<AccordionContextType | null>(null);
const ItemContext = createContext<{ value: string } | null>(null);

export function Accordion({ children }: { children: React.ReactNode }) {
  const [openValue, setOpenValue] = useState<string | null>(null);
  const toggle = (val: string) => setOpenValue((prev) => (prev === val ? null : val));

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

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 accCtx = useContext(AccordionContext);
  const itemCtx = useContext(ItemContext);
  if (!accCtx || !itemCtx) throw new Error("AccordionTrigger must be inside AccordionItem");

  const isOpen = accCtx.openValue === itemCtx.value;

  return (
    <button
      onClick={() => accCtx.toggle(itemCtx.value)}
      className="w-full flex justify-between items-center py-2 font-medium text-left"
    >
      {children}
      <span className="ml-2">{isOpen ? "▲" : "▼"}</span>
    </button>
  );
}

export function AccordionContent({ children }: { children: React.ReactNode }) {
  const accCtx = useContext(AccordionContext);
  const itemCtx = useContext(ItemContext);
  if (!accCtx || !itemCtx) throw new Error("AccordionContent must be inside AccordionItem");

  if (accCtx.openValue !== itemCtx.value) return null;

  return <div className="py-2 text-slate-600 text-sm">{children}</div>;
}

V-ați lovit vreodată de componente UI care devin un coșmar de mentenanță din cauza props-urilor opționale? Am pățit asta anul trecut pe o platformă cu 18k useri activi, unde o simplă componentă de tip <Accordion items={data} /> ajunsese să primească 14 props-uri diferite. Echipa de design cerea ba un buton separat în header, ba un badge lângă titlu, ba un stil diferit doar pentru al doilea element.

Fiecare excepție însemna un nou if în interiorul componentei sau un alt callback trimis din tată în fiu. Soluția curată pentru problema asta nu e mai mult cod condițional, ci pattern-ul de Compound Components. Este exact arhitectura pe care se bazează biblioteci ca Radix UI sau shadcn/ui.

De ce crapă arhitectura monolitică bazată pe props

Când pasăm un array de obiecte către o componentă rigidă, îi tăiem consumatorului libertatea de a controla structura DOM. Dacă vrei un <form> într-un item și un simplu text în altul, componenta monolitică devine un monstru.

Compound Components rezolvă asta prin împărțirea responsabilităților. Componenta părinte gestionează starea (care secțiune e deschisă), iar sub-componentele (Trigger, Content, Item) se ocupă exclusiv de afișare și interacțiune locală. Totul comunică transparent prin React Context.

Cum legăm Context-ul fără să scurgem re-render-uri inutile

Secretul este să izolăm starea globală a accordionului de starea fiecărui item. Avem un AccordionContext principal care ține minte doar ID-ul sau valoarea elementului activ. Apoi, fiecare AccordionItem își creează propriul context restrâns, care oferă valoarea curentă copiilor săi.

Atât AccordionTrigger, cât și AccordionContent consumă ambele contexte. Știu exact dacă sunt deschise sau închise fără să primească vreun prop de stare direct în JSX.

Aruncă o privire peste implementarea din snippet-ul de cod atașat. Observă cum AccordionTrigger aruncă o eroare explicită dacă cineva încearcă să îl folosească în afara unui AccordionItem. Asta salvează ore întregi de debugging pentru colegii din echipă.

Trade-off-uri sincere: flexibilitate versus verbozitate

Nu există glonț de argint în frontend, iar pattern-ul ăsta vine cu minusurile lui clare:

  • Merge bine pentru: Design systems, biblioteci interne de UI, componente complexe unde layout-ul variază des de la o pagină la alta.
  • Nasol pentru: Cazuri extrem de simple. Dacă ai doar 3 întrebări frecvente cu text static, să scrii 15 linii de JSX în loc de 3 e boilerplate pur.
  • Atenție la performanță: Dacă schimbi starea în părinte, toate componentele care consumă contextul se vor re-rendera. La un accordion simplu nu se simte, dar dacă ai 200 de itemi cu animații grele în listă, trebuie să memorezi contextul sau să folosești o librărie de stare atomică.

După ce am refăcut UI kit-ul nostru folosind pattern-ul ăsta, am redus PR-urile de refactoring pe componente de UI cu aproape 40%. Devii mult mai rapid când doar compui slot-uri în loc să modifici fișierul sursă al componentei de fiecare dată când apare o cerință nouă.

Voi ce abordare folosiți în proiectele voastre când design-ul o ia razna: mergeți pe compound components sau preferați componente headless gen TanStack / Radix?

Răspunsuri 0

Se încarcă răspunsurile…

Loghează-te pentru a răspunde

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