Webion

Compound components: guida all'implementazione

Scopri come implementare i compound components in React: criteri, esempi e best practice per creare widget complessi con API leggibili e manutenibili.

S
Saad Medhaton September 29, 2025
G22

All'interno dei nostri applicativi usiamo spesso il pattern dei compound components per costruire widget ricchi mantenendo una API compositiva e leggibile. In questo articolo raccolgo criteri, esempi e buone pratiche adottate nel progetto per chi deve scrivere o estendere componenti complessi senza sacrificare la developer experience.

Perche' scegliere i compound components

  • Evitano la proliferazione di props su componenti monolitici.
  • Permettono di condividere stato interno (loading, variant, interazioni) senza prop drilling.
  • Rendono piu' facile combinare porzioni opzionali (header, azioni, indicatori) mantenendo i fallback.

Anatomia del pattern

  1. Context privato e hook di accesso
    Ogni famiglia espone un context interno che custodisce lo stato condiviso. Pubblicare un hook useComponentContext con messaggio di errore esplicito aiuta chi usa il componente a capire subito se dimentica il wrapper root.


TYPESCRIPT
1// apps/app/components/QuoteMobileCard.tsx
2const QuoteMobileCardContext = createContext<QuoteMobileCardContextProps | undefined>(undefined);
3
4const useQuoteMobileCardContext = () => {
5 const context = useContext(QuoteMobileCardContext);
6
7 if (!context) {
8 throw new Error('QuoteMobileCard compound components must be used within a QuoteMobileCard provider');
9 }
10
11 return context;
12};


  1. Un Root che incapsula provider e layout
    Il root riceve le props comuni (loading, variant, handler di click) e decide il markup base, includendo il provider del context.
TYPESCRIPT
1// apps/app/components/QuoteMobileCard.tsx
2function QuoteMobileCardRoot({
3 children,
4 isLoading = false,
5 onClick,
6}: QuoteMobileCardRootProps) {
7 return (
8 <QuoteMobileCardContext.Provider value={{ isLoading }}>
9 <Stack
10 component={ButtonBase}
11 onClick={onClick}
12 >
13 {children}
14 </Stack>
15 </QuoteMobileCardContext.Provider>
16 );
17}


  1. Sotto-componenti consapevoli dello stato condiviso
    Ogni sub-component usa l'hook del context per leggere flag e decidere resa (ad esempio mostrare skeleton quando isLoading e' attivo).
TYPESCRIPT
1// apps/app/components/QuoteMobileCard.tsx
2const QuoteMobileCardTitle = ({ children }: { readonly children: React.ReactNode }) => {
3 const { isLoading } = useQuoteMobileCardContext();
4 return isLoading
5 ? <Skeleton variant="text" width="40%" />
6 : <Typography variant="h6">{children}</Typography>;
7};


  1. API finale: oggetto con le entry point
    In fondo al file esportiamo un oggetto che raggruppa Root e i componenti figli. Questo permette una sintassi scorrevole e auto-documentata.
TYPESCRIPT
1// apps/app/components/QuoteMobileCard.tsx
2export const QuoteMobileCard = {
3 Root: QuoteMobileCardRoot,
4 Main: QuoteMobileCardMain,
5 Title: QuoteMobileCardTitle,
6 Subtitle: QuoteMobileCardSubtitle,
7};


Esempi concreti nell'app


Qui sotto trovi un esempio pratico di composizione opzionale costruito sugli stessi tasselli visti sopra. Ogni blocco puo' essere inserito o omesso in base alla view corrente, mantenendo l'API dichiarativa.

TYPESCRIPT
1<QuoteMobileCard.Root isLoading={isLoading}>
2 <QuoteMobileCard.Title>
3 {quoteTitle}
4 </QuoteMobileCard.Title>
5
6 {showSubtitle && (
7 <QuoteMobileCard.Subtitle>
8 Aggiornato il {lastUpdate}
9 </QuoteMobileCard.Subtitle>
10 )}
11
12 <QuoteMobileCard.Main
13 name={storeCode}
14 infoString={description}
15 />
16</QuoteMobileCard.Root>


Per introdurre il tassello opzionale QuoteMobileCard.Subtitle basta definire un nuovo sotto-componente che legge il medesimo context e gestisce il fallback in fase di loading.


TYPESCRIPT
1const QuoteMobileCardTitle = ({ children }: { readonly children: React.ReactNode }) => {
2 const { isLoading } = useQuoteMobileCardContext();
3 return isLoading
4 ? <Skeleton variant="text" width="40%" sx={{ fontSize: '1rem' }} />
5 : <Typography variant="h6">{children}</Typography>;
6};
7
8const QuoteMobileCardSubtitle = ({ children }: { readonly children: React.ReactNode }) => {
9 const { isLoading } = useQuoteMobileCardContext();
10 return isLoading
11 ? <Skeleton variant="text" width="60%" sx={{ fontSize: '0.75rem' }} />
12 : <Typography variant="body2" color="text.secondary">{children}</Typography>;
13};
14
15export const QuoteMobileCard = {
16 Root: QuoteMobileCardRoot,
17 Main: QuoteMobileCardMain,
18 Title: QuoteMobileCardTitle,
19 Subtitle: QuoteMobileCardSubtitle,
20};

In questo modo chi utilizza il componente decide quali blocchi montare, senza propagare prop aggiuntive o scrivere condizionali sparsi nel layout.

Buone pratiche che manteniamo

  • Errori espliciti: ogni hook di context lancia un errore descrittivo.
  • Loading centralizzato: e' il root a decidere quando mostrare skeleton; i figli si limitano a interrogare il context.

Come costruire un nuovo compound component

  1. Definisci il context con il payload minimo (ad esempio { isLoading }).
  2. Scrivi Root avvolgendo Provider e markup di base.
  3. Esporre un hook useSomethingContext con errore chiaro.
  4. Crea i sotto-componenti che leggono dal context.
  5. Esporta tutto in un oggetto letterale ordinato.

Conclusione


Il pattern dei compound components ci permette di mantenere i layout complessi coerenti in tutta l'applicazione, riducendo prop drilling e migliorando la leggibilita'. Seguendo gli step descritti sopra possiamo introdurre nuovi widget composti garantendo la stessa DX.


Saad Medhat

La vecchia porta la sbarra