cloneElement
cloneElement vous permet de créer un élément React en vous basant sur un élément existant.
const clonedElement = cloneElement(element, props, ...children)Référence
cloneElement(element, props, ...children)
Appelez cloneElement pour créer un élément React basé sur element, mais avec des props (y compris children) distincts :
import { cloneElement } from 'react';
// ...
const clonedElement = cloneElement(
<Row title="Greeting">
Bonjour
</Row>,
{ isHighlighted: true },
'Au revoir'
);
console.log(clonedElement); // <Row title="Greeting" isHighlighted={true}>Au revoir</Row>Voir d’autres exemples ci-dessous.
Paramètres
-
element: l’argumentelementdoit être un élément React valide. Il peut par exemple s’agir d’un nœud JSX tel que<Something />ou du résultat d’un appel àcreateElementvoire d’un autre appel àcloneElement. -
props: l’argumentpropsdoit être soit un objet, soitnull. Si vous passeznull, l’élément cloné conservera toutes leselement.propsd’origine. Dans le cas contraire, pour chaque prop de l’objetprops, l’élément renvoyé « favorisera » la valeur issue depropsplutôt que celle issue d’element.props. Le reste des props seront remplies à partir deselement.propsd’origine. Si vous passezprops.keyouprops.ref, elles remplaceront également celles d’origine. -
...childrenoptionels : un nombre quelconque de nœuds enfants. Il peut s’agir de n’importe quels nœuds React, y compris des éléments React, des chaînes de caractères, des nombres, des portails, des nœuds vides (null,undefined,trueetfalse) et des tableaux de nœuds React. Si vous ne passez aucun argument...children, leselement.props.childrend’origine seront préservés.
Valeur renvoyée
cloneElement renvoie un objet descripteur d’élément React avec quelques propriétés :
type: identique àelement.type.props: le résultat d’une fusion superficielle deelement.propsavec lespropsprioritaires que vous auriez éventuellement passées.ref: laelement.refd’origine, à moins qu’elle n’ait été remplacée parprops.ref.key: laelement.keyd’origine, à moins qu’elle n’ait été remplacée parprops.key.
En général, vous renverrez l’élément depuis votre composant, ou en ferez l’enfant d’un autre élément. Même si vous pourriez lire les propriétés de l’élément, il vaut mieux traiter tout objet élément comme une boîte noire après sa création, et vous contenter de l’afficher.
Limitations
-
Le clonage d’un élément ne modifie pas l’élément d’origine.
-
Vous ne devriez passer les enfants comme arguments multiples à
cloneElementque s’ils sont statiquement connus, comme par exemplecloneElement(element, null, child1, child2, child3). Si vos enfants sont dynamiques, passez leur tableau entier comme troisième argument :cloneElement(element, null, listItems). Ça garantit que React vous avertira en cas dekeymanquantes lors de listes dynamiques. C’est inutile pour les listes statiques puisque leur ordre et leur taille ne changent jamais. -
cloneElementcomplexifie le pistage du flux de données, aussi vous devriez préférer ses alternatives.
Utilisation
Surcharger les props d’un élément
Pour surcharger les props d’un élément React, passez-le à cloneElement, conjointement aux props que vous souhaitez remplacer :
import { cloneElement } from 'react';
// ...
const clonedElement = cloneElement(
<Row title="Greeting" />,
{ isHighlighted: true }
);Ici, l’élément cloné sera <Row title="Greeting" isHighlighted={true} />.
Déroulons un exemple afin de comprendre en quoi c’est utile.
Imaginons qu’un composant List affiche ses children comme une liste de lignes sélectionnables avec un bouton « Suivant » qui modifie la ligne sélectionnée. Le composant List doit pouvoir afficher la Row sélectionnée d’une façon différente, il clone donc chaque enfant <Row> qu’il reçoit, et y ajoute une prop supplémentaire isHighlighted: true ou 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
})
)}Disons que le JSX d’origine reçu par List ressemble à ça :
<List>
<Row title="Chou" />
<Row title="Ail" />
<Row title="Pomme" />
</List>En clonant ses enfants, la List peut passer des infos supplémentaires à chaque Row qu’elle contient. Le résultat ressemblerait à ceci :
<List>
<Row
title="Chou"
isHighlighted={true}
/>
<Row
title="Ail"
isHighlighted={false}
/>
<Row
title="Pomme"
isHighlighted={false}
/>
</List>Voyez comme le fait de presser « Suivant » met à jour l’état de la List et met en exergue une ligne différente :
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) ); }}> Suivant </button> </div> ); }
En résumé, la List a cloné les éléments <Row /> qu’elle a reçus et leur a ajouté une prop supplémentaire.
Alternatives
Passer des données via une prop de rendu
Plutôt que d’utiliser cloneElement, envisagez d’accepter une prop de rendu (render prop, NdT) du genre renderItem. Ci-dessous, List reçoit une prop renderItem. List appelle renderItem pour chaque élément et lui passe isHighlighted comme argument :
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 est appelée « prop de rendu » parce que c’est une prop indiquant comment faire le rendu de quelque chose. Vous pouvez par exemple passer une implémentation de renderItem qui produit une <Row> avec la valeur isHighlighted reçue :
<List
items={products}
renderItem={(product, isHighlighted) =>
<Row
key={product.id}
title={product.title}
isHighlighted={isHighlighted}
/>
}
/>Le résultat final est identique à la version basée sur cloneElement :
<List>
<Row
title="Chou"
isHighlighted={true}
/>
<Row
title="Ail"
isHighlighted={false}
/>
<Row
title="Pomme"
isHighlighted={false}
/>
</List>En revanche, il est plus facile de pister l’origine de la valeur 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 ); }}> Suivant </button> </div> ); }
Cette approche est préférable à cloneElement car elle est plus explicite.
Passer des données via un contexte
Une autre alternative à cloneElement consiste à passer des données via un contexte.
Vous pourriez par exemple appeler createContext pour définir un HighlightContext :
export const HighlightContext = createContext(false);Votre composant List peut enrober chaque élément qu’il affiche dans un fournisseur de 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.Provider key={item.id} value={isHighlighted}>
{renderItem(item)}
</HighlightContext.Provider>
);
})}Avec cette approche, Row n’a même pas besoin de recevoir une prop isHighlighted. Il la lit plutôt directement depuis le contexte :
export default function Row({ title }) {
const isHighlighted = useContext(HighlightContext);
// ...Ça permet au composant appelant de ne pas avoir à se soucier de passer isHighlighted à <Row> :
<List
items={products}
renderItem={product =>
<Row title={product.title} />
}
/>List et Row coordonnent plutôt la logique de mise en exergue au travers du contexte.
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.Provider key={item.id} value={isHighlighted} > {renderItem(item)} </HighlightContext.Provider> ); })} <hr /> <button onClick={() => { setSelectedIndex(i => (i + 1) % items.length ); }}> Suivant </button> </div> ); }
Apprenez-en davantage sur la transmission de données via un contexte.
Extraire la logique dans un Hook personnalisé
Une autre approche que vous pouvez tenter consiste à extraire la logique « non visuelle » dans votre propre Hook, puis à utiliser l’information renvoyée par votre Hook pour décider du contenu de votre rendu. Vous pourriez par exemple écrire un Hook personnalisé useList comme celui-ci :
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];
}Puis vous l’utiliseriez comme suit :
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}>
Suivant
</button>
</div>
);
}Le flux de données est explicite, mais l’état réside dans le Hook personnalisé useList que vous pouvez réutiliser dans n’importe quel composant :
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}> Suivant </button> </div> ); }
Cette approche est particulièrement utile lorsque vous voulez réutiliser une même logique dans des composants distincts.