cloneElement
Using cloneElement
is uncommon and can lead to fragile code. See common alternatives.
cloneElement
lets you create a new React element using another element as a starting point.
const clonedElement = cloneElement(element, props, ...children)
Reference {/reference/}
cloneElement(element, props, ...children)
{/cloneelement/}
Call cloneElement
to create a React element based on the element
, but with different props
and 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>
Parameters {/parameters/}
-
element
: Theelement
argument must be a valid React element. For example, it could be a JSX node like<Something />
, the result of callingcreateElement
, or the result of anothercloneElement
call. -
props
: Theprops
argument must either be an object ornull
. If you passnull
, the cloned element will retain all of the originalelement.props
. Otherwise, for every prop in theprops
object, the returned element will “prefer” the value fromprops
over the value fromelement.props
. The rest of the props will be filled from the originalelement.props
. If you passprops.key
orprops.ref
, they will replace the original ones. -
optional
...children
: Zero or more child nodes. They can be any React nodes, including React elements, strings, numbers, portals, empty nodes (null
,undefined
,true
, andfalse
), and arrays of React nodes. If you don’t pass any...children
arguments, the originalelement.props.children
will be preserved.
Returns {/returns/}
cloneElement
returns a React element object with a few properties:
type
: Same aselement.type
.props
: The result of shallowly mergingelement.props
with the overridingprops
you have passed.ref
: The originalelement.ref
, unless it was overridden byprops.ref
.key
: The originalelement.key
, unless it was overridden byprops.key
.
Usually, you’ll return the element from your component or make it a child of another element. Although you may read the element’s properties, it’s best to treat every element as opaque after it’s created, and only render it.
Caveats {/caveats/}
-
Cloning an element does not modify the original element.
-
You should only pass children as multiple arguments to
cloneElement
if they are all statically known, likecloneElement(element, null, child1, child2, child3)
. If your children are dynamic, pass the entire array as the third argument:cloneElement(element, null, listItems)
. This ensures that React will warn you about missingkey
s for any dynamic lists. For static lists this is not necessary because they never reorder. -
cloneElement
makes it harder to trace the data flow, so try the alternatives instead.
Usage {/usage/}
Overriding props of an element {/overriding-props-of-an-element/}
To override the props of some cloneElement
with the
import { cloneElement } from 'react';
// ...const clonedElement = cloneElement( <Row title="Cabbage" />, { isHighlighted: true });
Here, the resulting <Row title="Cabbage" isHighlighted={true} />
.
Let’s walk through an example to see when it’s useful.
Imagine a List
component that renders its children
as a list of selectable rows with a “Next” button that changes which row is selected. The List
component needs to render the selected Row
differently, so it clones every <Row>
child that it has received, and adds an extra isHighlighted: true
or isHighlighted: false
prop:
export default function List({ children }) { const [selectedIndex, setSelectedIndex] = useState(0); return ( <div className="List"> {Children.map(children, (child, index) => cloneElement(child, { isHighlighted: index === selectedIndex }) )}
Let’s say the original JSX received by List
looks like this:
<List> <Row title="Cabbage" /> <Row title="Garlic" /> <Row title="Apple" /></List>
By cloning its children, the List
can pass extra information to every Row
inside. The result looks like this:
<List> <Row title="Cabbage" isHighlighted={true} /> <Row title="Garlic" isHighlighted={false} /> <Row title="Apple" isHighlighted={false} /></List>
Notice how pressing “Next” updates the state of the List
, and highlights a different row:
import List from './List.js';import Row from './Row.js';import { products } from './data.js';
export default function App() { return ( <List> {products.map(product => <Row key={product.id} title={product.title} /> )} </List> );}
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> );}
export default function Row({ title, isHighlighted }) { return ( <div className={[ 'Row', isHighlighted ? 'RowHighlighted' : '' ].join(' ')}> {title} </div> );}
export const products = [ { title: 'Cabbage', id: 1 }, { title: 'Garlic', id: 2 }, { title: 'Apple', id: 3 },];
.List { display: flex; flex-direction: column; border: 2px solid grey; padding: 5px;}
.Row { border: 2px dashed black; padding: 5px; margin: 5px;}
.RowHighlighted { background: #ffa;}
button { height: 40px; font-size: 20px;}
To summarize, the List
cloned the <Row />
elements it received and added an extra prop to them.
Cloning children makes it hard to tell how the data flows through your app. Try one of the alternatives.
Alternatives {/alternatives/}
Passing data with a render prop {/passing-data-with-a-render-prop/}
Instead of using cloneElement
, consider accepting a render prop like renderItem
. Here, List
receives renderItem
as a prop. List
calls renderItem
for every item and passes isHighlighted
as an argument:
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); })}
The renderItem
prop is called a “render prop” because it’s a prop that specifies how to render something. For example, you can pass a renderItem
implementation that renders a <Row>
with the given isHighlighted
value:
<List items={products} renderItem={(product, isHighlighted) => <Row key={product.id} title={product.title} isHighlighted={isHighlighted} /> }/>
The end result is the same as with cloneElement
:
<List> <Row title="Cabbage" isHighlighted={true} /> <Row title="Garlic" isHighlighted={false} /> <Row title="Apple" isHighlighted={false} /></List>
However, you can clearly trace where the isHighlighted
value is coming from.
import List from './List.js';import Row from './Row.js';import { products } from './data.js';
export default function App() { return ( <List items={products} renderItem={(product, isHighlighted) => <Row key={product.id} title={product.title} isHighlighted={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> );}
export default function Row({ title, isHighlighted }) { return ( <div className={[ 'Row', isHighlighted ? 'RowHighlighted' : '' ].join(' ')}> {title} </div> );}
export const products = [ { title: 'Cabbage', id: 1 }, { title: 'Garlic', id: 2 }, { title: 'Apple', id: 3 },];
.List { display: flex; flex-direction: column; border: 2px solid grey; padding: 5px;}
.Row { border: 2px dashed black; padding: 5px; margin: 5px;}
.RowHighlighted { background: #ffa;}
button { height: 40px; font-size: 20px;}
This pattern is preferred to cloneElement
because it is more explicit.
Passing data through context {/passing-data-through-context/}
Another alternative to cloneElement
is to pass data through context.
For example, you can call createContext
to define a HighlightContext
:
export const HighlightContext = createContext(false);
Your List
component can wrap every item it renders into a HighlightContext
provider:
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> ); })}
With this approach, Row
does not need to receive an isHighlighted
prop at all. Instead, it reads the context:
export default function Row({ title }) { const isHighlighted = useContext(HighlightContext); // ...
This allows the calling component to not know or worry about passing isHighlighted
to <Row>
:
<List items={products} renderItem={product => <Row title={product.title} /> }/>
Instead, List
and Row
coordinate the highlighting logic through context.
import List from './List.js';import Row from './Row.js';import { products } from './data.js';
export default function App() { return ( <List items={products} renderItem={(product) => <Row title={product.title} /> } /> );}
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> );}
import { useContext } from 'react';import { HighlightContext } from './HighlightContext.js';
export default function Row({ title }) { const isHighlighted = useContext(HighlightContext); return ( <div className={[ 'Row', isHighlighted ? 'RowHighlighted' : '' ].join(' ')}> {title} </div> );}
import { createContext } from 'react';
export const HighlightContext = createContext(false);
export const products = [ { title: 'Cabbage', id: 1 }, { title: 'Garlic', id: 2 }, { title: 'Apple', id: 3 },];
.List { display: flex; flex-direction: column; border: 2px solid grey; padding: 5px;}
.Row { border: 2px dashed black; padding: 5px; margin: 5px;}
.RowHighlighted { background: #ffa;}
button { height: 40px; font-size: 20px;}
Learn more about passing data through context.
Extracting logic into a custom Hook {/extracting-logic-into-a-custom-hook/}
Another approach you can try is to extract the “non-visual” logic into your own Hook, and use the information returned by your Hook to decide what to render. For example, you could write a useList
custom Hook like this:
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];}
Then you could use it like this:
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> );}
The data flow is explicit, but the state is inside the useList
custom Hook that you can use from any component:
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> );}
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];}
export default function Row({ title, isHighlighted }) { return ( <div className={[ 'Row', isHighlighted ? 'RowHighlighted' : '' ].join(' ')}> {title} </div> );}
export const products = [ { title: 'Cabbage', id: 1 }, { title: 'Garlic', id: 2 }, { title: 'Apple', id: 3 },];
.List { display: flex; flex-direction: column; border: 2px solid grey; padding: 5px;}
.Row { border: 2px dashed black; padding: 5px; margin: 5px;}
.RowHighlighted { background: #ffa;}
button { height: 40px; font-size: 20px;}
This approach is particularly useful if you want to reuse this logic between different components.