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

Cum scrii Compound Components flexibile: Rețeta din spatele shadcn/ui

De Bogdan Răducanu, 30 mai 2026 · 5 vizualizări · 3 like-uri

Postat 30 mai 2026
typescript
import React, { createContext, useContext, useState } from 'react';

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

export function Accordion({ children }: { children: React.ReactNode }) {
  const [active, setActive] = useState<string | null>(null);
  const toggle = (val: string) => setActive(active === val ? null : val);
  return (
    <AccordionContext.Provider value={{ active, toggle }}>
      <div className="border rounded-md divide-y">{children}</div>
    </AccordionContext.Provider>
  );
}

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

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 itemValue = useContext(ItemContext);
  if (!group || !itemValue) throw new Error('Must be inside AccordionItem');

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

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

  if (group.active !== itemValue) return null;
  return <div className="pb-2 text-gray-600 text-sm">{children}</div>;
}

Dacă ai lucrat vreodată la o librărie internă de UI, știi coșmarul componentelor care cresc ca un monstru. Adaugi un prop pentru iconiță, unul pentru padding, încă trei pentru animații și te trezești cu o componentă imposibil de citit. Este un iad de întreținut.

Am pățit exact asta acum vreo doi ani, la un proiect SaaS cu peste 15k de utilizatori activi. Designerul schimba layout-ul taburilor și al acordeoanelor de la un sprint la altul. Într-o parte aveam nevoie de iconiță în stânga, în altă parte trebuia un badge roșu lângă titlu, iar în altă pagină trebuia ca tot rândul să fie dezactivat. Am început să scriem cod condiționat în draci până când codul a devenit de necitit. Atunci am zis stop și am rescris totul folosind compound components, inspirat de ce fac cei de la Radix UI și, ulterior, shadcn/ui.

Ce este, de fapt, Compound Components Pattern?

În loc să pasezi toată configurația prin props către o singură componentă mamut, împarți responsabilitățile în mai multe sub-componente. Acestea colaborează în spate prin intermediul unui context React, fără ca tu să pasezi manual starea de la una la alta.

Utilizatorul final obține un control total asupra markup-ului și a stilizării, scriind cod declarativ direct în JSX. Practic, transformi o componentă rigidă într-un set de piese de LEGO.

Cum construim un Accordion flexibil

Pentru a implementa acest pattern, avem nevoie de un context părinte care să știe ce element este deschis în momentul respectiv. Apoi, fiecare item va avea propriul său context local ca să-și cunoască propria cheie.

Uită-te la codul din secțiunea de mai jos ca să vezi cât de curat se împart responsabilitățile. Părintele Accordion gestionează starea globală (care item e deschis), AccordionItem oferă ID-ul unic în arbore, iar AccordionTrigger și AccordionContent consumă aceste informații pentru a decide când să se afișeze sau ce clasă de CSS să aplice.

Trade-off-urile de care trebuie să fii conștient

Ca orice decizie de arhitectură, și asta vine cu un preț. Nimic nu e gratis pe lumea asta.

Avantajul major este flexibilitatea absolută. Dacă mâine vrei să pui un buton de editare în interiorul trigger-ului sau un tooltip ciudat, o poți face direct în JSX, fără să adaugi vreun prop nou în componenta de bază. Markup-ul îți aparține.

Dezavantajul principal este că pierzi din controlul structurii. Un developer mai junior poate să pună din greșeală AccordionContent în afara unui AccordionItem, iar TypeScript nu te va ajuta la compilare în mod nativ. Te vei trezi cu erori la runtime dacă nu pui validări bune în hook-urile de context (cum este eroarea aruncată în codul de mai jos).

În plus, codul devine puțin mai verbose în locurile în care este utilizat. În loc de o singură linie de cod, vei scrie șase sau șapte linii de JSX.

Voi cum gestionați componentele astea complexe în proiectele voastre? Mergeți pe flexibilitate maximă ca în shadcn sau preferați o structură mai rigidă, dar mai sigură?

Răspunsuri 0

Se încarcă răspunsurile…

Loghează-te pentru a răspunde

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