eduardweb.
Hooks & PatternsAvansat#architecture#typescript#react#frontend

Cum scrii Compound Components în React fără să o iei razna: Pattern-ul din spatele shadcn

De Delia Petre, 16 iun. 2026 · 14 vizualizări · 2 like-uri

Postat 16 iun. 2026
typescript
import React, { createContext, useContext, useState } from 'react';

const AccordionContext = createContext<{
  activeValue: string | null;
  setActiveValue: (val: string | null) => void;
} | null>(null);

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

export function Accordion({ children, defaultValue = null }: { children: React.ReactNode; defaultValue?: string | null }) {
  const [activeValue, setActiveValue] = useState<string | null>(defaultValue);
  return (
    <AccordionContext.Provider value={{ activeValue, setActiveValue }}>
      <div className="border rounded-md divide-y">{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 group = useContext(AccordionContext);
  const value = useContext(ItemContext);
  if (!group || !value) throw new Error('AccordionTrigger must be used inside AccordionItem');
  
  const isOpen = group.activeValue === value;
  return (
    <button 
      onClick={() => group.setActiveValue(isOpen ? null : value)}
      className="w-full text-left font-medium py-2 flex justify-between"
      aria-expanded={isOpen}
    >
      {children}
    </button>
  );
}

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

  if (group.activeValue !== value) return null;
  return <div className="py-2 text-gray-600">{children}</div>;
}

Am refăcut acum o lună un sistem de filtre pentru un SaaS cu vreo 12k utilizatori activi. Codul vechi era un coșmar: un singur mega-component cu vreo 15 props de configurare. Am trecut totul pe compound components, în stilul folosit de Radix UI sau shadcn, și am redus complexitatea codului la jumătate.

De ce ne batem capul cu asta?

Să zicem că ai de făcut un <Accordion>. Varianta naivă e să-i dai un array de obiecte ca prop. Sună bine la prima vedere, până când design-ul se schimbă și cere ca al treilea rând să aibă un badge roșu, iar al cincilea să fie dezactivat.

Atunci începi să adaugi props noi: renderHeader, disabledIndexes, badgeColors. Felicitări, ai creat un monstru greu de întreținut.

Pattern-ul de compound components sparge componenta în bucăți atomice care colaborează între ele prin context. În loc de o singură componentă mamut, expui mai multe componente mici: Accordion, AccordionItem, AccordionTrigger și AccordionContent. Utilizatorul bibliotecii tale are acum control total asupra markup-ului și stilizării, fără ca tu să mai schimbi o linie de cod în logica internă.

Cum funcționează sub capotă?

Secretul este să nu expui starea direct către exterior. Părintele (Accordion) gestionează starea (care item este deschis în momentul de față) și oferă o metodă de update prin context.

Fiecare AccordionItem își creează propriul sub-context ca să își propage valoarea unică (un value sau id) către trigger și conținut. Astfel, triggerul știe ce valoare să trimită când se face click, iar conținutul știe dacă trebuie să se randeze sau nu.

Am folosit abordarea asta la un proiect intern și am economisit cam 30% din timpul de dezvoltare pe partea de UI. Dezvoltatorii de pe frontend n-au mai trebuit să ne ceară modificări în componente de fiecare dată când se schimba un design în Figma.

Trade-off-ul sincer

Nimic nu e gratis în arhitectura software. Da, obții o flexibilitate incredibilă, dar ai și câteva dezavantaje.

În primul rând, ai mult mai mult boilerplate. Scrii mai multe fișiere sau cel puțin mai multe funcții în același fișier.

În al doilea rând, strici "încapsularea" simplă. Un junior poate uita să pună un AccordionTrigger înăuntrul unui AccordionItem, ceea ce va arunca o eroare de runtime dacă nu ai grijă să pui niște guard clauses solide în useContext. De asemenea, dacă ai mii de astfel de componente pe o pagină (deși e un caz extrem și nerealist), update-urile de context pot genera re-randări inutile. Pentru 99% din aplicații însă, performanța e absolut impecabilă.

Cum adăugăm accesibilitate fără dureri de cap

Un acordeon bun nu înseamnă doar niște div-uri care se ascund. Ai nevoie de atributele ARIA corecte pentru screen readere.

Folosind sub-contextul din AccordionItem, putem genera automat ID-uri unice folosind hook-ul useId din React. Legăm aria-controls de pe buton de id-ul div-ului de conținut. Astfel, accesibilitatea vine direct "out of the box", fără ca cel care folosește componenta să știe măcar ce sunt acele atribute.

Voi folosiți pattern-ul ăsta în codebase-urile voastre sau preferați componentele clasice, cu props rigide, dar mai greu de stricat de către restul echipei?

Răspunsuri 0

Se încarcă răspunsurile…

Loghează-te pentru a răspunde

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