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 argumentoelement
deve 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 argumentoprops
deve ser um objeto ounull
. Se você passarnull
, o elemento clonado reterá todas aselement.props
originais. Caso contrário, para cada prop no objetoprops
, o elemento retornado irá “preferir” o valor deprops
sobre o valor deelement.props
. O restante das props será preenchido a partir doelement.props
original. Se você passarprops.key
ouprops.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
,true
efalse
), e arrays de nós React. Se você não passar nenhum argumento...children
, oelement.props.children
original será preservado.
Retorna
cloneElement
retorna um objeto Elemento React com algumas propriedades:
type
: Igual aelement.type
.props
: O resultado da mesclagem rasa deelement.props
com asprops
que você passou.ref
: Oelement.ref
original, a menos que fosse sobreposto porprops.ref
.key
: Oelement.key
original, 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
children
como vários argumentos paracloneElement
se eles forem todos estaticamente conhecidos, comocloneElement(element, null, child1, child2, child3)
. Se seuschildren
forem dinâmicos, passe o array inteiro como o terceiro argumento:cloneElement(element, null, listItems)
. Isso garante que o React irá avisá-lo sobrekey
s ausentes para quaisquer listas dinâmicas. Para listas estáticas, isso não é necessário porque elas nunca são reordenadas. -
cloneElement
dificulta 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.Provider key={item.id} value={isHighlighted}>
{renderItem(item)}
</HighlightContext.Provider>
);
})}
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.Provider key={item.id} value={isHighlighted} > {renderItem(item)} </HighlightContext.Provider> ); })} <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.