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’argumentelement
doit ê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 àcreateElement
voire d’un autre appel àcloneElement
. -
props
: l’argumentprops
doit être soit un objet, soitnull
. Si vous passeznull
, l’élément cloné conservera toutes leselement.props
d’origine. Dans le cas contraire, pour chaque prop de l’objetprops
, l’élément renvoyé « favorisera » la valeur issue deprops
plutôt que celle issue d’element.props
. Le reste des props seront remplies à partir deselement.props
d’origine. Si vous passezprops.key
ouprops.ref
, elles remplaceront également celles d’origine. -
...children
optionels : 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
,true
etfalse
) et des tableaux de nœuds React. Si vous ne passez aucun argument...children
, leselement.props.children
d’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.props
avec lesprops
prioritaires que vous auriez éventuellement passées.ref
: laelement.ref
d’origine, à moins qu’elle n’ait été remplacée parprops.ref
.key
: laelement.key
d’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 à
cloneElement
que 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 dekey
manquantes lors de listes dynamiques. C’est inutile pour les listes statiques puisque leur ordre et leur taille ne changent jamais. -
cloneElement
complexifie 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.