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

Compound Components în React: Cum construiești un Accordion flexibil stil shadcn/ui

De Diana Oprea, 22 iul. 2026 · 4 vizualizări · 2 like-uri

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

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

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

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

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

export const AccordionItem = ({ value, children }: { value: string; children: React.ReactNode }) => (
  <ItemContext.Provider value={{ value }}>
    <div className="group">{children}</div>
  </ItemContext.Provider>
);

export const AccordionTrigger = ({ children }: { children: React.ReactNode }) => {
  const acc = useContext(AccordionContext);
  const item = useContext(ItemContext);
  if (!acc || !item) throw new Error('AccordionTrigger se folosește doar în AccordionItem');

  const isOpen = acc.value === item.value;
  return (
    <button onClick={() => acc.toggle(item.value)} className="w-full flex justify-between p-4 font-medium">
      {children}
      <span>{isOpen ? '−' : '+'}</span>
    </button>
  );
};

De ce ne încurcăm în prop-uri?

M-am lovit de zeci de ori în proiecte de clasicul <Accordion data={items} renderItem={...} allowMultiple={true} />. La început arată curat în JSX. Dar după două sprinturi, designerul vrea un icon diferit pe un singur item, un badge lângă titlu doar dacă e o alertă, și un dropdown de acțiuni în dreapta header-ului.

Rezultatul? Adaugi itemHeaderRightSlot, customIconFn, badgeCondition și alte 10 prop-uri. Componenta devine un monstru imposibil de întreținut.

Anul trecut, când am refăcut o secție critică din aplicație la un proiect cu peste 14k utilizatori activi, am eliminat complet abordarea asta monolitică în favoarea pattern-ului de Compound Components. Am rescris kit-ul de UI intern și am redus codul duplicat din pagini cu aproape 25%, având în același timp libertate totală pe partea de markup.

Anatomia unui Compound Component cu Context

Ideea din spatele shadcn/ui sau Radix UI nu e vreo magie, ci pură compoziție. Împarți componenta în bucăți atomice care comunică între ele printr-un React Context ascuns.

Avem nevoie de:

  1. Accordion (root-ul care ține starea globală de deschidere)
  2. AccordionItem (context local pentru un element specific, ține value-ul lui)
  3. AccordionTrigger (butonul care comută starea)
  4. AccordionContent (panoul care se afișează sau ascunde)

Fără prop-drilling și fără să forțezi o structură rigidă în DOM. Dacă vrei să pui un <div> decorativ sau o linie de separare între Trigger și Content, poți să o faci fără să spargi logica.

Trucul din spate: Custom Hooks cu verificare de Context

Cea mai frecventă greșeală pe care o văd când devii implementează pattern-ul ăsta e că uită să protejeze contextul. Dacă cineva folosește <AccordionTrigger /> în afara unui <AccordionItem />, aplicația crapă silențios sau dă erori ciudate de React.

Secretul e un hook intern simplu care aruncă o eroare explicită în dev mode dacă contextul e null:

const useAccordionItemContext = () => {
  const context = useContext(AccordionItemContext);
  if (!context) {
    throw new Error('AccordionTrigger/Content trebuie folosit în interiorul unui AccordionItem');
  }
  return context;
};

Asta salvează ore bune de debugging colegilor din echipă care folosesc componenta fără să-i știe neapărat arhitectura internă.

Trade-off-uri sincere: Când merită și când nu?

Nu totul e lapte și miere. Există un motiv pentru care nu orice componentă trebuie scrisă așa.

Când e perfect:

  • Design systems, UI kit-uri interne, componente reutilizabile în tot proiectul.
  • Când ai nevoie de acces la slot-uri flexibile (iconițe, badge-uri, butoane în header).
  • Când vrei un DX impecabil în JSX.

Când e overkill:

  • Pentru un widget banal de FAQ unde conținutul e 100% static și nu se va schimba niciodată structura visuală.
  • Dacă echipa ta e la început cu React; JSX-ul mai verbos poate fi intimidant pentru cineva obișnuit doar să paseze un array de obiecte.

Marele câștig e că decuplezi logica de stare de structura vizuală. Voi ce folosiți în producție: primitive gen Radix/shadcn direct sau vă scrieți propriile compoziții?

Răspunsuri 0

Se încarcă răspunsurile…

Loghează-te pentru a răspunde

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