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 baseado no element
, mas com props
e children
diferentes:
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 acloneElement
. -
props
: O argumentoprops
deve ser um objeto ounull
. Se você passarnull
, o elemento clonado irá reter todas as propriedades doelement
original. Caso contrário, para cada prop no objetoprops
, o elemento retornado “preferirá” o valor deprops
sobre o valor deelement.props
. As demais props serão preenchidas a partir deelement.props
original. Se você passarprops.key
ouprops.ref
, eles substituirão os 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
, aschildren
do elemento originalelement.props
serão preservadas.
Retorna
cloneElement
retorna um objeto de elemento React com algumas propriedades:
type
: Igual aelement.type
.props
: O resultado da mesclagem superficial deelement.props
com asprops
que você passou.ref
: Aelement.ref
original, a menos que tenha sido substituída porprops.ref
.key
: Aelement.key
original, a menos que tenha sido substituída porprops.key
.
Geralmente, você retornará o elemento do seu componente ou o tornará um 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 passar filhos como múltiplos argumentos para
cloneElement
apenas se todos eles forem estaticamente conhecidos, comocloneElement(element, null, child1, child2, child3)
. Se seus filhos forem dinâmicos, passe o array inteiro como o terceiro argumento:cloneElement(element, null, listItems)
. Isso garante que o React irá avisá-lo sobre chaves faltando para qualquer lista dinâmica. Para listas estáticas, isso não é necessário porque elas nunca reordenam. -
cloneElement
torna mais difícil rastrear o fluxo de dados, então tente as alternativas em vez disso.
Uso
Sobrescrevendo props de um elemento
Para sobrescrever as props de algum elemento React, passe-o para cloneElement
com as props que você deseja sobrescrever:
import { cloneElement } from 'react';
// ...
const clonedElement = cloneElement(
<Row title="Cabbage" />,
{ isHighlighted: true }
);
Aqui, o elemento clonado resultante será <Row title="Cabbage" isHighlighted={true} />
.
Vamos passar por 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 está selecionada. O componente List
precisa renderizar a linha Row
selecionada 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
})
)}
Vamos supor que o JSX original recebido por List
pareça assim:
<List>
<Row title="Cabbage" />
<Row title="Garlic" />
<Row title="Apple" />
</List>
Ao clonar seus filhos, List
pode passar informações extras para cada Row
dentro. O resultado parece assim:
<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) ); }}> Próximo </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 de renderItem
que renderiza um <Row>
com o valor isHighlighted
dado:
<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 vem o 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 ); }}> Próximo </button> </div> ); }
Esse padrão é preferido ao cloneElement
porque é mais explícito.
Passando dados através do contexto
Outra alternativa ao 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 envolver 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
de forma alguma. Em vez disso, ele lê o contexto:
export default function Row({ title }) {
const isHighlighted = useContext(HighlightContext);
// ...
Isso permite que o componente chamador 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 destaque através 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 ); }}> Próximo </button> </div> ); }
Saiba mais sobre passar dados através do contexto.
Extraindo 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ê poderia 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];
}
Então você poderia 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}>
Próximo
</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}> Próximo </button> </div> ); }
Essa abordagem é particularmente útil se você deseja reutilizar essa lógica entre diferentes componentes.