cloneElement
cloneElement ti permette di creare un nuovo elemento React usando un altro elemento come punto di partenza.
const clonedElement = cloneElement(element, props, ...children)Reference
cloneElement(element, props, ...children)
Chiama cloneElement per creare un elemento React basato su element, ma con props e children diversi:
import { cloneElement } from 'react';
// ...
const clonedElement = cloneElement(
<Row title="Cabbage">
Hello
</Row>,
{ isHighlighted: true },
'Goodbye'
);
console.log(clonedElement); // <Row title="Cabbage" isHighlighted={true}>Goodbye</Row>Parameters
-
element: L’argomentoelementdeve essere un elemento React valido. Ad esempio, può essere un nodo JSX come<Something />, il risultato di una chiamata acreateElemento il risultato di un’altra chiamata acloneElement. -
props: L’argomentopropsdeve essere un oggetto oppurenull. Se passinull, l’elemento clonato conserverà tutte leelement.propsoriginali. Altrimenti, per ogni prop nell’oggettoprops, l’elemento restituito “preferirà” il valore dapropsrispetto al valore daelement.props. Le altre props verranno prese dalleelement.propsoriginali. Se passiprops.keyoprops.ref, sostituiranno quelli originali. -
optional
...children: Zero o più nodi figli. Possono essere qualsiasi nodo React, inclusi elementi React, stringhe, numeri, portali, nodi vuoti (null,undefined,trueefalse) e array di nodi React. Se non passi argomenti...children, verranno preservati glielement.props.childrenoriginali.
Returns
cloneElement restituisce un oggetto elemento React con alcune proprietà:
type: Uguale aelement.type.props: Il risultato della fusione superficiale dielement.propscon lepropsdi override che hai passato.ref: Laelement.reforiginale, a meno che non sia stata sovrascritta daprops.ref.key: Laelement.keyoriginale, a meno che non sia stata sovrascritta daprops.key.
Di solito restituirai l’elemento dal tuo componente o lo renderai come figlio di un altro elemento. Anche se puoi leggere le proprietà dell’elemento, è meglio trattare ogni elemento come opaco dopo la creazione e limitarti a renderizzarlo.
Caveats
-
Clonare un elemento non modifica l’elemento originale.
-
Dovresti passare i children come argomenti multipli a
cloneElementsolo se sono tutti staticamente noti, comecloneElement(element, null, child1, child2, child3). Se i tuoi children sono dinamici, passa l’intero array come terzo argomento:cloneElement(element, null, listItems). In questo modo React ti avviserà dellekeymancanti per qualsiasi lista dinamica. Per le liste statiche non è necessario, perché non vengono mai riordinate. -
cloneElementrende più difficile tracciare il flusso dei dati, quindi prova le alternative.
Usage
Sovrascrivere le props di un elemento
Per sovrascrivere le props di un elemento React, passalo a cloneElement con le props che vuoi sovrascrivere:
import { cloneElement } from 'react';
// ...
const clonedElement = cloneElement(
<Row title="Cabbage" />,
{ isHighlighted: true }
);Qui, l’elemento clonato risultante sarà <Row title="Cabbage" isHighlighted={true} />.
Vediamo un esempio per capire quando è utile.
Immagina un componente List che renderizza i suoi children come lista di righe selezionabili con un pulsante “Next” che cambia quale riga è selezionata. Il componente List deve renderizzare in modo diverso la Row selezionata, quindi clona ogni child <Row> che ha ricevuto e aggiunge una prop extra isHighlighted: true o isHighlighted: false:
export default function List({ children }) {
const [selectedIndex, setSelectedIndex] = useState(0);
return (
<div className="List">
{Children.map(children, (child, index) =>
cloneElement(child, {
isHighlighted: index === selectedIndex
})
)}Supponiamo che il JSX originale ricevuto da List sia questo:
<List>
<Row title="Cabbage" />
<Row title="Garlic" />
<Row title="Apple" />
</List>Clonando i suoi children, List può passare informazioni extra a ogni Row al suo interno. Il risultato è simile a questo:
<List>
<Row
title="Cabbage"
isHighlighted={true}
/>
<Row
title="Garlic"
isHighlighted={false}
/>
<Row
title="Apple"
isHighlighted={false}
/>
</List>Nota come premere “Next” aggiorna lo state di List ed evidenzia una riga diversa:
import { Children, cloneElement, useState } from 'react'; export default function List({ children }) { const [selectedIndex, setSelectedIndex] = useState(0); return ( <div className="List"> {Children.map(children, (child, index) => cloneElement(child, { isHighlighted: index === selectedIndex }) )} <hr /> <button onClick={() => { setSelectedIndex(i => (i + 1) % Children.count(children) ); }}> Next </button> </div> ); }
In sintesi, List ha clonato gli elementi <Row /> che ha ricevuto e ha aggiunto loro una prop extra.
Alternatives
Passare dati con una render prop
Invece di usare cloneElement, valuta di accettare una render prop come renderItem. Qui, List riceve renderItem come prop. List chiama renderItem per ogni elemento e passa isHighlighted come argomento:
export default function List({ items, renderItem }) {
const [selectedIndex, setSelectedIndex] = useState(0);
return (
<div className="List">
{items.map((item, index) => {
const isHighlighted = index === selectedIndex;
return renderItem(item, isHighlighted);
})}La prop renderItem si chiama “render prop” perché è una prop che specifica come renderizzare qualcosa. Ad esempio, puoi passare un’implementazione di renderItem che renderizza una <Row> con il valore isHighlighted dato:
<List
items={products}
renderItem={(product, isHighlighted) =>
<Row
key={product.id}
title={product.title}
isHighlighted={isHighlighted}
/>
}
/>Il risultato finale è lo stesso di cloneElement:
<List>
<Row
title="Cabbage"
isHighlighted={true}
/>
<Row
title="Garlic"
isHighlighted={false}
/>
<Row
title="Apple"
isHighlighted={false}
/>
</List>Tuttavia, puoi tracciare chiaramente da dove proviene il valore isHighlighted.
import { useState } from 'react'; export default function List({ items, renderItem }) { const [selectedIndex, setSelectedIndex] = useState(0); return ( <div className="List"> {items.map((item, index) => { const isHighlighted = index === selectedIndex; return renderItem(item, isHighlighted); })} <hr /> <button onClick={() => { setSelectedIndex(i => (i + 1) % items.length ); }}> Next </button> </div> ); }
Questo pattern è preferibile a cloneElement perché è più esplicito.
Passare dati tramite context
Un’altra alternativa a cloneElement è passare dati tramite context.
Ad esempio, puoi chiamare createContext per definire un HighlightContext:
export const HighlightContext = createContext(false);Il tuo componente List può avvolgere ogni elemento che renderizza in un provider HighlightContext:
export default function List({ items, renderItem }) {
const [selectedIndex, setSelectedIndex] = useState(0);
return (
<div className="List">
{items.map((item, index) => {
const isHighlighted = index === selectedIndex;
return (
<HighlightContext key={item.id} value={isHighlighted}>
{renderItem(item)}
</HighlightContext>
);
})}Con questo approccio, Row non deve ricevere affatto una prop isHighlighted. Legge invece il context:
export default function Row({ title }) {
const isHighlighted = useContext(HighlightContext);
// ...In questo modo il componente chiamante non deve conoscere o preoccuparsi di passare isHighlighted a <Row>:
<List
items={products}
renderItem={product =>
<Row title={product.title} />
}
/>Invece, List e Row coordinano la logica di evidenziazione tramite context.
import { useState } from 'react'; import { HighlightContext } from './HighlightContext.js'; export default function List({ items, renderItem }) { const [selectedIndex, setSelectedIndex] = useState(0); return ( <div className="List"> {items.map((item, index) => { const isHighlighted = index === selectedIndex; return ( <HighlightContext key={item.id} value={isHighlighted} > {renderItem(item)} </HighlightContext> ); })} <hr /> <button onClick={() => { setSelectedIndex(i => (i + 1) % items.length ); }}> Next </button> </div> ); }
Scopri di più sul passaggio di dati tramite context.
Estrarre la logica in un custom hook
Un altro approccio che puoi provare è estrarre la logica “non visuale” in un tuo Hook e usare le informazioni restituite dall’Hook per decidere cosa renderizzare. Ad esempio, puoi scrivere un custom hook useList come questo:
import { useState } from 'react';
export default function useList(items) {
const [selectedIndex, setSelectedIndex] = useState(0);
function onNext() {
setSelectedIndex(i =>
(i + 1) % items.length
);
}
const selected = items[selectedIndex];
return [selected, onNext];
}Poi puoi usarlo così:
export default function App() {
const [selected, onNext] = useList(products);
return (
<div className="List">
{products.map(product =>
<Row
key={product.id}
title={product.title}
isHighlighted={selected === product}
/>
)}
<hr />
<button onClick={onNext}>
Next
</button>
</div>
);
}Il flusso dei dati è esplicito, ma lo state è dentro il custom hook useList che puoi usare da qualsiasi componente:
import Row from './Row.js'; import useList from './useList.js'; import { products } from './data.js'; export default function App() { const [selected, onNext] = useList(products); return ( <div className="List"> {products.map(product => <Row key={product.id} title={product.title} isHighlighted={selected === product} /> )} <hr /> <button onClick={onNext}> Next </button> </div> ); }
Questo approccio è particolarmente utile se vuoi riutilizzare questa logica tra componenti diversi.