Atención

El uso de cloneElement no es común y puede conducir a un código frágil. Mira alternativas comunes.

cloneElement te permite crear un nuevo elemento de React usando otro elemento como punto de partida.

const clonedElement = cloneElement(element, props, ...children)

Uso

Sobreescribiendo propiedades de un elemento

Para sobreescribir las propiedades de algún React element, pásalo a cloneElement con las propiedades que quieres sobreescribir:

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>

Observe cómo al presionar “Next” 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.

Atención

Clonando los hijos hace difícil saber cómo fluye la información a través de tu aplicación. Intenta una de las alternativas.


Alternativas

Pasando datos con una render prop

En vez de usar cloneElement, considera aceptar una render prop como renderItem. Aquí, List recibe renderItem como una propiedad. 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 propiedad 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.


Pasando 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 el 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.


Extrayendo 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];
}

Entonces 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.


Referencia

cloneElement(element, props, ...children)

Llama a cloneElement para crear un elemento React basado en el element, 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>

Ver más ejemplos arriba.

Parámetros

  • element: El argumento element debe ser un elemento React válido. Por ejemplo, podría ser un nodo JSX como <Something />, el resultado de llamar a createElement, o el resultado de otra llamada a cloneElement.

  • props: El argumento props debe ser un objeto o null. Si pasas null, el elemento clonado mantendrá todos los element.props originales. De lo contrario, para cada propiedad en el objeto props, el elemento devuelto “preferirá” el valor de props sobre el valor de element.props. El resto de las propiedades se completarán a partir de los element.props originales. Si pasas props.key o props.ref, reemplazarán a los originales.

  • opcional ...children: Cero o más nodos hijo. Pueden ser cualquier nodo React, incluidos elementos React, cadenas, números, portales, nodos vacíos (null, undefined, true, y false), y matrices de nodos React. Si no pasas ningún argumento ...children, se conservarán los element.props.children originales.

Devuelve

cloneElement devuelve un objeto de elemento React con algunas propiedades:

  • type: Igual que element.type.
  • props: El resultado de mezclar superficialmente element.props con los props que has pasado para sobrescribirlos.
  • ref: El element.ref original, a menos que se haya anulado con props.ref.
  • key: El element.key original, a menos que se haya anulado con props.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 createElement si todos son conocidos estáticamente, como cloneElement(element, null, child1, child2, child3). Si tus hijos son dinámicos, pasa toda la matriz como tercer argumento: cloneElement(element, null, listItems). Esto garantiza que React te advertirá sobre las keys que faltan para cualquier lista dinámica. Para listas estáticas no es necesario porque nunca se reordenan.

  • cloneElement hace que sea más difícil rastrear el flujo de datos, por lo que intente las alternativas en su lugar.