In React è possibile annidare i componenti. All’interno del metodo render() di un componente possiamo usare uno o più componenti predefiniti o creati da noi. È possibile annidare Functional Component all’interno di Class Component e viceversa.
Riprendiamo un esempio già visto nei precedenti articoli.
const Button = props => <button className={`btn btn--${props.color}`} >click</button>;
const App = props => (
<div>
<Button color="indigo" />
<Button color="cyan" />
<Button color="light-blue" />
</div>
)
ReactDOM.render(<App />, rootNode);
Il Functional component App restituisce un React Element div che ha come diretti discendenti tre elementi <Button /> costruiti sulla base del Functional Component Button da noi definito. Per ora ci siamo limitati a copiare e incollare lo stesso elemento più volte, cambiando il valore della proprietà ‘color’ anche se non è una buona pratica.
Lista di componenti dello stesso tipo
Una possibile alternativa è quella di utilizzare un array di valori e la funzione Array.prototype.map() per costruire la struttura del nostro componente in maniera dinamica. Riscriviamo l’esempio di sopra.
const colors = ['indigo', 'cyan', 'light-blue'];
const Button = props => <button className={`btn btn--${props.color}`} >click</button>;
const App = ({colors}) => {
// buttons è un array di Elementi React <Button />
const buttons = colors.map(color => <Button color={color} />);
return (
<div>
{buttons}
</div>
)
}
ReactDOM.render(<App colors={colors} />, rootNode);
Applichiamo la funzione Array.prototype.map() all’array colors. Ad ogni iterazione Array.prototype.map() riceve come primo parametro un elemento dell’array colors e restituisce un React Element costruito utilizzando il componente Button da noi definito. Al termine della funzione map(), la variabile buttons sarà un array di Elementi React. Tramite interpolazione, inseriamo i pulsanti all’interno di un elemento div. Se aprite la console degli strumenti per sviluppatori del browser, noterete però un errore.
React ci avvisa che ogni elemento del nostro array deve avere una prop "key" di tipo stringa che sia unica fra gli altri elementi del medesimo array. Possiamo comunque riusare la stessa prop "key" all’interno di due array differenti. Correggiamo l’esempio appena visto. Nel nostro caso possiamo passare uno dei colori come prop "key".
const colors = ['indigo', 'cyan', 'light-blue'];
const Button = props => <button className={`btn btn--${props.color}`} >click</button>;
const App = ({colors}) => {
// buttons è un array di Elementi React Button
const buttons = colors.map(color => <Button key={color} color={color} />);
return (
<div>
{buttons}
</div>
)
}
ReactDOM.render(<App colors={colors} />, rootNode);
React utilizza la proprietà "key" per identificare quale elemento rimuovere, aggiornare o aggiungere all’interno del VirtualDOM. Proprio per questo motivo bisogna aggiungere una proprietà "key" agli elementi all’interno dell’array che abbiamo costruito.
Comporre i Componenti tramite this.props.children
Finora abbiamo usato JSX per creare degli elementi e abbiamo utilizzato dei tag del tipo <Button /> con la coppia di caratteri "/>" a chiudere il tag. Babel tradurrà questo tag in una chiamata alla funzione React.createElement(Button, null). Il React Element costruito sulla base del componente "Button" non presenta degli elementi discendenti. A volte può però essere utile realizzare strutture più complesse e annidare i React Element così come siamo soliti fare con gli elementi HTML.
Consideriamo l’esempio di un componente che chiameremo Card e consideriamo il seguente codice scritto utilizzando la sintassi JSX. Creiamo quindi un React Element <Card></Card> a partire dal componente Card. Questo nuovo elemento React avrà fra i suoi discendenti tre React Element, due dei quali (Header, Footer) saranno creati sulla base dei componenti da noi definiti.
<Card>
{/*
L'elemento Card avrà degli Elementi annidati. Due di questi elementi React saranno
costruiti sulla base di Componenti React da noi definiti.
*/}
<Header text="Header 1"/>
<p>Nullam quis risus eget urna mollis ornare vel eu leo.</p>
<Footer text="Contenuto del Footer"/>
</Card>
All’interno del componente Card possiamo accedere agli elementi annidati attraverso l’uso di props.children. Creiamo innanzitutto il componente Card.
const Card = props => <div>{props.children}</div>;
Creiamo ora i due componenti Header e Footer.
const Header = props => <h1>{props.text}</h1>;
const Footer = props => <p>{props.text}</p>;
E infine creiamo un componente App che useremo poi nella funzione ReactDOM.render().
const App = props => (
<Card>
{/*
L'elemento Card avrà degli Elementi annidati. Due di questi elementi React saranno
costruiti sulla base di Componenti React da noi definiti.
*/}
<Header text="Header 1"/>
<p>Nullam quis risus eget urna mollis ornare vel eu leo.</p>
<Footer text="Contenuto del Footer"/>
</Card>
)
ReactDOM.render(<App />, rootNode);
All’interno del componente Card, possiamo chiamare console.dir(props.children) e quella che segue e un’immagine di ciò che viene mostrato nella console di Chrome.
Children Utilities
Abbiamo visto come utilizzare props.children all’interno dei nostri componenti. Consideriamo un altro esempio. Creiamo un componente Menu all’interno del quale utilizzeremo props.children per accedere ai link (elementi <a href="">Nome link</a>.) che passeremo come discendenti dell’Elemento React <Menu></Menu>.
const Menu = props => {
/*
* child.props.children corrisponde al testo di ciascun link
* per cui otterremo "Home" per il primo link, Contatti per il secondo ecc...
*/
const listItems = props.children.map(child => <li key={child.props.children}>{child}</li>);
return (
<ul>
{listItems}
</ul>
)
}
const App = props => (
<Menu>
<a href="/">Home</a>
<a href="/contatti/">Contatti</a>
<a href="/servizi/">Servizi</a>
</Menu>
)
ReactDOM.render(<App />, rootNode);
Fin qui tutto funziona perfettamente, all’interno della nostra pagina vedremo una lista di link.
Ma se <Menu></Menu> ha un solo discendente, cosa succede?
<Menu>
<a href="/">Home</a>
</Menu>
Nella console riceveremo un messaggio di errore!
Il motivo dell’errore è che nel primo caso avevamo un array di oggetti.
Nel secondo caso abbiamo un singolo oggetto e, quindi, non è possibile invocare la funzione Array.prototype.map() visto che in questo caso props.children non è un Array.
Proprio per risolvere questo tipo di situazioni, ci viene in aiuto l’API di React. Al posto di props.children possiamo utilizzare React.Children che ci fornisce una serie di metodi per lavorare più facilmente con props.children. Uno dei metodi che possiamo usare per risolvere l’errore dell’esempio visto sopra è React.Children.map(children, fn).
const Menu = props => {
const listItems = React.Children.map(
props.children,
child => <li key={child.props.childre>{child}</li>
);
return (
<ul>
{listItems}
</ul>
)
}
const App = props => (
<Menu>
<a href="/">Home</a>
</Menu>
)
ReactDOM.render(<App />, rootNode);
In questo caso anche con un solo link, non avremo nessun problema. React.Children fornisce altri metodi interessanti come forEach(), count() e toArray(). Per maggiori dettagli potete consultare la documentazione ufficiale di React.
L’ultima interessante funzione che vedremo in questo articolo è React.cloneElement(element, [props], […children]) che clona e restituisce un nuovo elemento React utilizzando come punto di partenza l’elemento passato come primo argomento. Le props, passate come secondo argomento, saranno combinate con le proprietà dell’elemento originale. Se passiamo il terzo argomento opzionale children, questo sostituirà gli eventuali elementi figli dell’elemento di partenza.
Questa funzione può essere particolarmente utile qualora, all’interno di un componente, si vogliano passare delle props agli elementi contenuti in props.children.
Riprendiamo l’esempio visto sopra e modifichiamolo usando React.cloneElement().
const ListItem = props => <li>{props.children}</li>;
const Menu = props => {
const func = child => React.cloneElement(<ListItem>{child}</ListItem>, {
key: child.props.children,
otherProp: "Prop passata da Menu"
});
const listItems = React.Children.map(props.children, func);
return (
<ul>
{listItems}
</ul>
)
}
const App = props => (
<Menu>
<a href="/">Home</a>
<a href="/contatti/">Contatti</a>
<a href="/servizi/">Servizi</a>
</Menu>
)
ReactDOM.render(<App />, rootNode);
Definiamo tre componenti: App, Menu e ListItem. All’interno del componente Menu avremo accesso a props.children che sarà un array di elementi React, corrispondenti ai link discendenti dell’elemento <Menu> </Menu>. All’interno del componente Menu, usiamo la funzione React.Children.map() e passiamo props.children come primo argomento. Il secondo argomento sarà una funzione che riceverà, uno alla volta, gli elementi dell’array props.children. Ogni elemento sarà inserito come discendente di <ListItem>, passato poi come primo argomento della funzione React.cloneElement(). Il risultato di ogni chiamata alla funzione func() sarà un nuovo elemento <ListItem /> con attributi key e otherProp che saranno accessibili, all’interno del componente ListItem attraverso l’oggetto props.
Se volete maggiori informazioni, trovate tutti i dettagli necessari nella documentazione ufficiale (React.cloneElement()).
Nel prossimo articolo parleremo del ciclo di vita di un componente React.