Children e Composizione
La Prop children
In React, children è una prop speciale che rappresenta il contenuto inserito tra i tag di apertura e chiusura di un componente. È il meccanismo fondamentale per la composizione dei componenti.
function Card({ children }) {
return <div className="card">{children}</div>;
}
// Uso: tutto ciò che sta tra <Card> e </Card> diventa children
function App() {
return (
<Card>
<h2>Titolo</h2>
<p>Questo è il contenuto della card.</p>
</Card>
);
}
Tipi di children
La prop children può essere qualsiasi tipo renderizzabile da React.
// Stringa
<Wrapper>Testo semplice</Wrapper>
// Elementi JSX
<Wrapper>
<h1>Titolo</h1>
<p>Paragrafo</p>
</Wrapper>
// Espressioni
<Wrapper>{isLogged && <ProfiloUtente />}</Wrapper>
// Array di elementi
<Wrapper>
{utenti.map(u => <Utente key={u.id} nome={u.nome} />)}
</Wrapper>
// Misto
<Wrapper>
<h1>Titolo</h1>
Testo libero
{variabile}
<Footer />
</Wrapper>
Tipizzare children con TypeScript
// Per qualsiasi contenuto renderizzabile
interface Props {
children: React.ReactNode;
}
// Solo elementi React (non stringhe o numeri)
interface Props {
children: React.ReactElement;
}
// Un singolo elemento figlio
interface Props {
children: React.ReactElement;
}
// Funzione come children (render prop)
interface Props {
children: (dati: DatiType) => React.ReactNode;
}
React.ReactNode e’ il tipo piu’ comune e include: elementi JSX, stringhe, numeri, booleani, null, undefined e array di questi tipi.
Composizione vs Ereditarietà
React favorisce fortemente la composizione rispetto all’ereditarietà. La documentazione ufficiale afferma che Facebook usa React in migliaia di componenti senza aver mai trovato un caso d’uso in cui l’ereditarietà fosse preferibile.
Contenimento (Containment)
Alcuni componenti non conoscono in anticipo il loro contenuto. Usano children per accettare qualsiasi contenuto.
function Pannello({ titolo, children }) {
return (
<div className="pannello">
<div className="pannello-header">
<h2>{titolo}</h2>
</div>
<div className="pannello-body">
{children}
</div>
</div>
);
}
function App() {
return (
<Pannello titolo="Impostazioni">
<label>
Nome: <input type="text" />
</label>
<label>
Email: <input type="email" />
</label>
<button>Salva</button>
</Pannello>
);
}
Slot Multipli (Named Slots)
Quando servono più “buchi” in un componente, usa props dedicate invece di un singolo children.
interface LayoutProps {
header: React.ReactNode;
sidebar: React.ReactNode;
children: React.ReactNode;
footer?: React.ReactNode;
}
function Layout({ header, sidebar, children, footer }: LayoutProps) {
return (
<div className="layout">
<header className="layout-header">{header}</header>
<div className="layout-body">
<aside className="layout-sidebar">{sidebar}</aside>
<main className="layout-main">{children}</main>
</div>
{footer && <footer className="layout-footer">{footer}</footer>}
</div>
);
}
function App() {
return (
<Layout
header={<Navbar />}
sidebar={<MenuLaterale />}
footer={<PiePagina />}
>
<h1>Contenuto Principale</h1>
<p>Benvenuto nella mia applicazione.</p>
</Layout>
);
}
Specializzazione
Un componente “generico” viene specializzato creando componenti piu’ specifici che lo configurano.
// Componente generico
function Dialogo({ titolo, messaggio, colore, children }) {
return (
<div className="dialogo" style={{ borderColor: colore }}>
<h2>{titolo}</h2>
<p>{messaggio}</p>
<div className="dialogo-azioni">{children}</div>
</div>
);
}
// Specializzazione: Dialogo di Benvenuto
function DialogoBenvenuto() {
return (
<Dialogo
titolo="Benvenuto!"
messaggio="Grazie per aver visitato il nostro sito."
colore="#3b82f6"
>
<button>Continua</button>
</Dialogo>
);
}
// Specializzazione: Dialogo di Errore
function DialogoErrore({ errore, onRiprova }) {
return (
<Dialogo
titolo="Errore"
messaggio={errore}
colore="#ef4444"
>
<button onClick={onRiprova}>Riprova</button>
<button>Ignora</button>
</Dialogo>
);
}
Pattern di Composizione Avanzati
Compound Components
I Compound Components sono un pattern in cui più componenti lavorano insieme per formare un’unica interfaccia coesa, condividendo stato implicito.
import { createContext, useContext, useState } from "react";
// Context interno per condividere stato
const AccordionContext = createContext(null);
function Accordion({ children }) {
const [aperto, setAperto] = useState<string | null>(null);
return (
<AccordionContext.Provider value={{ aperto, setAperto }}>
<div className="accordion">{children}</div>
</AccordionContext.Provider>
);
}
function AccordionItem({ id, children }) {
return <div className="accordion-item">{children}</div>;
}
function AccordionTrigger({ id, children }) {
const { aperto, setAperto } = useContext(AccordionContext);
return (
<button
className="accordion-trigger"
onClick={() => setAperto(aperto === id ? null : id)}
aria-expanded={aperto === id}
>
{children}
</button>
);
}
function AccordionContent({ id, children }) {
const { aperto } = useContext(AccordionContext);
if (aperto !== id) return null;
return <div className="accordion-content">{children}</div>;
}
// Assegna sotto-componenti
Accordion.Item = AccordionItem;
Accordion.Trigger = AccordionTrigger;
Accordion.Content = AccordionContent;
// Uso pulito e dichiarativo
function App() {
return (
<Accordion>
<Accordion.Item id="1">
<Accordion.Trigger id="1">Sezione 1</Accordion.Trigger>
<Accordion.Content id="1">
Contenuto della sezione 1
</Accordion.Content>
</Accordion.Item>
<Accordion.Item id="2">
<Accordion.Trigger id="2">Sezione 2</Accordion.Trigger>
<Accordion.Content id="2">
Contenuto della sezione 2
</Accordion.Content>
</Accordion.Item>
</Accordion>
);
}
Render Props
Le render props sono un pattern in cui un componente riceve una funzione come prop (spesso children) e la chiama per determinare cosa renderizzare.
interface MouseTrackerProps {
children: (posizione: { x: number; y: number }) => React.ReactNode;
}
function MouseTracker({ children }: MouseTrackerProps) {
const [posizione, setPosizione] = useState({ x: 0, y: 0 });
const handleMouseMove = (e: React.MouseEvent) => {
setPosizione({ x: e.clientX, y: e.clientY });
};
return (
<div onMouseMove={handleMouseMove} style={{ height: "100vh" }}>
{children(posizione)}
</div>
);
}
// Uso: il consumatore decide cosa renderizzare con i dati
function App() {
return (
<MouseTracker>
{({ x, y }) => (
<p>
Il mouse è in posizione ({x}, {y})
</p>
)}
</MouseTracker>
);
}
Render Props vs Custom Hooks
I custom hooks hanno in gran parte sostituito le render props. Lo stesso esempio con un hook:
function useMousePosition() {
const [posizione, setPosizione] = useState({ x: 0, y: 0 });
useEffect(() => {
const handler = (e: MouseEvent) => {
setPosizione({ x: e.clientX, y: e.clientY });
};
window.addEventListener("mousemove", handler);
return () => window.removeEventListener("mousemove", handler);
}, []);
return posizione;
}
// Uso: più semplice e pulito
function App() {
const { x, y } = useMousePosition();
return <p>Il mouse è in posizione ({x}, {y})</p>;
}
Higher-Order Components (HOC)
Un HOC è una funzione che prende un componente e ne restituisce uno nuovo con funzionalità aggiuntive. È un pattern legacy ma ancora presente in molte codebase.
function conAutenticazione(Componente) {
return function ComponenteProtetto(props) {
const { utente, caricamento } = useAuth();
if (caricamento) return <Spinner />;
if (!utente) return <Redirect to="/login" />;
return <Componente {...props} utente={utente} />;
};
}
// Uso
const DashboardProtetta = conAutenticazione(Dashboard);
function App() {
return <DashboardProtetta />;
}
Oggi si preferiscono i custom hooks per la stessa logica, in quanto più semplici e componibili.
Manipolazione di children
React fornisce utility per lavorare programmaticamente con i children.
import { Children, cloneElement } from "react";
function GruppoBottoni({ children }) {
return (
<div className="gruppo-bottoni" role="group">
{Children.map(children, (child, index) => {
if (!child) return null;
return cloneElement(child, {
className: `${child.props.className || ""} btn-gruppo`,
style: {
...child.props.style,
marginLeft: index > 0 ? "8px" : "0",
},
});
})}
</div>
);
}
function App() {
return (
<GruppoBottoni>
<button>Uno</button>
<button>Due</button>
<button>Tre</button>
</GruppoBottoni>
);
}
API di React.Children
// Conta i figli
React.Children.count(children);
// Itera sui figli
React.Children.forEach(children, (child) => { ... });
// Mappa sui figli (restituisce un nuovo array)
React.Children.map(children, (child) => { ... });
// Converte children in un array piatto
React.Children.toArray(children);
// Verifica che ci sia un solo figlio
React.Children.only(children);
Best Practices
- Preferisci la composizione all’ereditarietà in ogni caso
- Usa
childrenper componenti contenitore generici - Usa props nominate (slot) quando servono più punti di inserimento
- Preferisci i custom hooks alle render props e agli HOC per condividere logica
- Tipizza sempre
childrencon TypeScript usandoReact.ReactNode - Evita la manipolazione diretta di children (
cloneElement,Children.map) quando possibile: rende il codice fragile
Riepilogo
La composizione è il paradigma fondamentale di React. La prop children permette di creare componenti flessibili e riutilizzabili. I pattern come Compound Components, render props e slot multipli offrono soluzioni eleganti per interfacce complesse. In React moderno, i custom hooks hanno sostituito molti pattern di composizione per la condivisione di logica tra componenti.