cloneElement
cloneElement te permite crear un nuevo elemento de React usando otro elemento como punto de partida.
const clonedElement = cloneElement(element, props, ...children)Referencia
cloneElement(elemento, props, ...children)
Llama a cloneElement para crear un elemento React basado en el elemento, pero con diferentes props y children:
import { cloneElement } from 'react';
// ...
const clonedElement = cloneElement(
<Row title="Cabbage">
Hello
</Row>,
{ isHighlighted: true },
'Goodbye'
);
console.log(clonedElement); // <Row title="Cabbage">Goodbye</Row>Parámetros
-
elemento: El argumentoelementodebe ser un elemento de React válido. Por ejemplo, podría ser un nodo JSX como<Something />, el resultado de llamar acreateElement, o el resultado de otra llamada acloneElement. -
props: El argumentopropsdebe ser un objeto onull. Si pasasnull, el elemento clonado mantendrá todos loselement.propsoriginales. De lo contrario, para cada propiedad en el objetoprops, el elemento devuelto “preferirá” el valor depropssobre el valor deelement.props. El resto de las propiedades se completarán a partir de loselement.propsoriginales. Si pasasprops.keyoprops.ref, reemplazarán a los originales. -
opcional
...children: Cero o más nodos hijo. Pueden ser cualquier nodo React, incluidos elementos de React, cadenas, números, portales, nodos vacíos (null,undefined,true, yfalse), y arrays de nodos de React. Si no pasas ningún argumento...children, se conservarán loselement.props.childrenoriginales.
Devuelve
cloneElement devuelve un objeto de elemento de React con algunas propiedades:
type: Igual queelement.type.props: El resultado de mezclar superficialmenteelement.propscon laspropsque has pasado para sobrescribirlas.ref: Elelement.reforiginal, a menos que se haya sobrescrito conprops.ref.key: Elelement.keyoriginal, a menos que se haya sobrescrito conprops.key.
Usualmente, devolverás el elemento desde tu componente o lo harás hijo de otro elemento. Aunque puedes leer las propiedades del elemento, es mejor tratar a cada elemento como opaco después de que se crea, y solo renderizarlo.
Advertencias
-
Clonar un elemento no modifica el elemento original.
-
Solo debes pasar hijos como múltiples argumentos a
createElementsi todos son conocidos estáticamente, comocloneElement(element, null, child1, child2, child3). Si tus hijos son dinámicos, pasa todo el array como tercer argumento:cloneElement(element, null, listItems). Esto garantiza que React te advierta sobre laskeys que faltan para cualquier lista dinámica. Para listas estáticas no es necesario porque nunca se reordenan. -
cloneElementhace que sea más difícil rastrear el flujo de datos, por lo que prueba las alternativas en su lugar.
Uso
Sobrescribir props de un elemento
Para sobrescribir las props de algún elemento de React, pásalo a cloneElement con las props que quieres sobrescribir:
import { cloneElement } from 'react';
// ...
const clonedElement = cloneElement(
<Row title="Cabbage" />,
{ isHighlighted: true }
);Aquí, el elemento clonado resultante será <Row title="Cabbage" isHighlighted={true} />.
Veamos un ejemplo para ver cuándo es útil.
Imagina un componente List que renderiza sus children como una lista de filas seleccionables con un botón “Next” que cambia qué fila está seleccionada. El componente List necesita renderizar la Row seleccionada de manera diferente, por lo que clona cada hijo <Row> que ha recibido y agrega una propiedad 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
})
)}Digamos que el JSX original recibido por List se ve así:
<List>
<Row title="Cabbage" />
<Row title="Garlic" />
<Row title="Apple" />
</List>Clonando sus hijos, List puede pasar información adicional a cada Row dentro. El resultado se ve así:
<List>
<Row
title="Cabbage"
isHighlighted={true}
/>
<Row
title="Garlic"
isHighlighted={false}
/>
<Row
title="Apple"
isHighlighted={false}
/>
</List>Observa cómo al presionar “Next” se actualiza el estado del List, y resalta una fila diferente:
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> ); }
Para resumir, List clonó los elementos <Row /> que recibió y les agregó una propiedad extra.
Alternativas
Pasar datos con una prop de renderizado
En vez de usar cloneElement, considera aceptar una render prop (o prop de renderizado) como renderItem. Aquí, List recibe renderItem como una prop. List llama a renderItem para cada elemento y pasa isHighlighted como un argumento:
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 se llama una “render prop” porque es una propiedad que especifica cómo renderizar algo. Por ejemplo, puedes pasar una implementación de renderItem que renderice un <Row> con el valor isHighlighted dado:
<List
items={products}
renderItem={(product, isHighlighted) =>
<Row
key={product.id}
title={product.title}
isHighlighted={isHighlighted}
/>
}
/>El resultado final es el mismo que con cloneElement:
<List>
<Row
title="Cabbage"
isHighlighted={true}
/>
<Row
title="Garlic"
isHighlighted={false}
/>
<Row
title="Apple"
isHighlighted={false}
/>
</List>Sin embargo, puedes rastrear claramente de dónde viene el valor 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> ); }
Este patrón es preferido a cloneElement porque es más explícito.
Pasar datos a través del contexto
Otra alternativa a cloneElement es pasar datos a través del contexto.
Por ejemplo, puedes llamar a createContext para definir un HighlightContext:
export const HighlightContext = createContext(false);Tu componente List puede envolver cada elemento que renderiza en un proveedor 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>
);
})}Con este enfoque, Row no necesita recibir una propiedad isHighlighted en absoluto. En su lugar, lee del contexto:
export default function Row({ title }) {
const isHighlighted = useContext(HighlightContext);
// ...Esto permite que el componente que llama no sepa o se preocupe por pasar isHighlighted a <Row>:
<List
items={products}
renderItem={product =>
<Row title={product.title} />
}
/>En vez de eso, List y Row coordinan la lógica de resaltado a través del contexto.
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 ); }}> Next </button> </div> ); }
Aprende más sobre pasar datos a través del contexto.
Extraer lógica en un Hook personalizado
Otro enfoque que puedes probar es extraer la lógica “no visual” en tu propio Hook, y usar la información devuelta por tu Hook para decidir qué renderizar. Por ejemplo, puedes escribir un Hook personalizado useList como este:
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];
}Luego puedes usarlo así:
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>
);
}El flujo de datos es explícito, pero el estado está dentro del Hook personalizado useList que puedes usar desde cualquier 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> ); }
Este enfoque es particularmente útil si quieres reutilizar esta lógica entre diferentes componentes.