cloneElement

Nota bene

Questa pagina è stata tradotta automaticamente e supervisionata da un maintainer. Un’ulteriore revisione da parte della community sarebbe comunque utile. Migliora questa traduzione.

Insidia

L’uso di cloneElement è poco comune e può portare a codice fragile. Vedi le alternative comuni.

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>

Vedi altri esempi sotto.

Parameters

  • element: L’argomento element deve essere un elemento React valido. Ad esempio, può essere un nodo JSX come <Something />, il risultato di una chiamata a createElement o il risultato di un’altra chiamata a cloneElement.

  • props: L’argomento props deve essere un oggetto oppure null. Se passi null, l’elemento clonato conserverà tutte le element.props originali. Altrimenti, per ogni prop nell’oggetto props, l’elemento restituito “preferirà” il valore da props rispetto al valore da element.props. Le altre props verranno prese dalle element.props originali. Se passi props.key o props.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, true e false) e array di nodi React. Se non passi argomenti ...children, verranno preservati gli element.props.children originali.

Returns

cloneElement restituisce un oggetto elemento React con alcune proprietà:

  • type: Uguale a element.type.
  • props: Il risultato della fusione superficiale di element.props con le props di override che hai passato.
  • ref: La element.ref originale, a meno che non sia stata sovrascritta da props.ref.
  • key: La element.key originale, a meno che non sia stata sovrascritta da props.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 cloneElement solo se sono tutti staticamente noti, come cloneElement(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à delle key mancanti per qualsiasi lista dinamica. Per le liste statiche non è necessario, perché non vengono mai riordinate.

  • cloneElement rende 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.

Insidia

Clonare i children rende difficile capire come i dati scorrono nella tua app. Prova una delle alternative.


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.