cloneElement
cloneElement permite que você crie um novo Elemento React usando outro elemento como ponto de partida.
const clonedElement = cloneElement(element, props, ...children)Referência
cloneElement(element, props, ...children)
Chame cloneElement para criar um Elemento React com base no element, mas com diferentes props e children:
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>Parâmetros
-
element: O argumentoelementdeve ser um Elemento React válido. Por exemplo, pode ser um nó JSX como<Something />, o resultado da chamadacreateElement, ou o resultado de outra chamada decloneElement. -
props: O argumentopropsdeve ser um objeto ounull. Se você passarnull, o elemento clonado reterá todas aselement.propsoriginais. Caso contrário, para cada prop no objetoprops, o elemento retornado irá “preferir” o valor depropssobre o valor deelement.props. O restante das props será preenchido a partir doelement.propsoriginal. Se você passarprops.keyouprops.ref, eles substituirão as originais. -
opcional
...children: Zero ou mais nós filhos. Eles podem ser quaisquer nós React, incluindo Elementos React, strings, números, portais, nós vazios (null,undefined,trueefalse), e arrays de nós React. Se você não passar nenhum argumento...children, oelement.props.childrenoriginal será preservado.
Retorna
cloneElement retorna um objeto Elemento React com algumas propriedades:
type: Igual aelement.type.props: O resultado da mesclagem rasa deelement.propscom aspropsque você passou.ref: Oelement.reforiginal, a menos que fosse sobreposto porprops.ref.key: Oelement.keyoriginal, a menos que fosse sobreposto porprops.key.
Geralmente, você retornará o elemento do seu componente ou o tornará filho de outro elemento. Embora você possa ler as propriedades do elemento, é melhor tratar cada elemento como opaco após sua criação e apenas renderizá-lo.
Ressalvas
-
Clonar um elemento não modifica o elemento original.
-
Você deve apenas passar
childrencomo vários argumentos paracloneElementse eles forem todos estaticamente conhecidos, comocloneElement(element, null, child1, child2, child3). Se seuschildrenforem dinâmicos, passe o array inteiro como o terceiro argumento:cloneElement(element, null, listItems). Isso garante que o React irá avisá-lo sobrekeys ausentes para quaisquer listas dinâmicas. Para listas estáticas, isso não é necessário porque elas nunca são reordenadas. -
cloneElementdificulta o rastreamento do fluxo de dados, então tente as alternativas em vez disso.
Uso
Substituindo as props de um elemento
Para substituir as props de algum Elemento React, passe-o para cloneElement com as props que você deseja substituir:
import { cloneElement } from 'react';
// ...
const clonedElement = cloneElement(
<Row title="Cabbage" />,
{ isHighlighted: true }
);Aqui, o elemento clonado resultante será <Row title="Cabbage" isHighlighted={true} />.
Vamos analisar um exemplo para ver quando isso é útil.
Imagine um componente List que renderiza seus children como uma lista de linhas selecionáveis com um botão “Próximo” que altera qual linha é selecionada. O componente List precisa renderizar o Row selecionado de forma diferente, então ele clona cada filho <Row> que recebeu e adiciona uma prop extra 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
})
)}Digamos que o JSX original recebido por List se pareça com isto:
<List>
<Row title="Cabbage" />
<Row title="Garlic" />
<Row title="Apple" />
</List>Ao clonar seus filhos, o List pode passar informações extras para cada Row dentro. O resultado tem esta aparência:
<List>
<Row
title="Cabbage"
isHighlighted={true}
/>
<Row
title="Garlic"
isHighlighted={false}
/>
<Row
title="Apple"
isHighlighted={false}
/>
</List>Observe como pressionar “Próximo” atualiza o estado do List e destaca uma linha 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, o List clonou os elementos <Row /> que recebeu e adicionou uma prop extra a eles.
Alternativas
Passando dados com uma render prop
Em vez de usar cloneElement, considere aceitar uma render prop como renderItem. Aqui, List recebe renderItem como uma prop. List chama renderItem para cada item e passa isHighlighted como um 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);
})}A prop renderItem é chamada de “render prop” porque é uma prop que especifica como renderizar algo. Por exemplo, você pode passar uma implementação renderItem que renderiza um <Row> com o valor isHighlighted fornecido:
<List
items={products}
renderItem={(product, isHighlighted) =>
<Row
key={product.id}
title={product.title}
isHighlighted={isHighlighted}
/>
}
/>O resultado final é o mesmo que com cloneElement:
<List>
<Row
title="Cabbage"
isHighlighted={true}
/>
<Row
title="Garlic"
isHighlighted={false}
/>
<Row
title="Apple"
isHighlighted={false}
/>
</List>No entanto, você pode rastrear claramente de onde o valor isHighlighted vem.
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> ); }
Esse padrão é preferido ao cloneElement porque é mais explícito.
Passando dados através do contexto
Outra alternativa para cloneElement é passar dados através do contexto.
Por exemplo, você pode chamar createContext para definir um HighlightContext:
export const HighlightContext = createContext(false);Seu componente List pode encapsular cada item que renderiza em um provedor 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>
);
})}Com essa abordagem, Row não precisa receber uma prop isHighlighted . Em vez disso, ele lê o contexto:
export default function Row({ title }) {
const isHighlighted = useContext(HighlightContext);
// ...Isso permite que o componente de chamada não saiba ou se preocupe em passar isHighlighted para <Row>:
<List
items={products}
renderItem={product =>
<Row title={product.title} />
}
/>Em vez disso, List e Row coordenam a lógica de realce por meio do 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 key={item.id} value={isHighlighted} > {renderItem(item)} </HighlightContext> ); })} <hr /> <button onClick={() => { setSelectedIndex(i => (i + 1) % items.length ); }}> Next </button> </div> ); }
Aprenda mais sobre como passar dados pelo contexto.
Extraindo a lógica em um Hook personalizado
Outra abordagem que você pode tentar é extrair a lógica “não visual” em seu próprio Hook e usar as informações retornadas pelo seu Hook para decidir o que renderizar. Por exemplo, você pode escrever um Hook personalizado useList assim:
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];
}Em seguida, você pode usá-lo assim:
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>
);
}O fluxo de dados é explícito, mas o estado está dentro do Hook personalizado useList que você pode usar de qualquer 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> ); }
Essa abordagem é particularmente útil se você quiser reutilizar essa lógica entre diferentes componentes.