2020-09-03
|~4 min read
|621 words
I recently came across some code making use of the cloneElement
function from React to pass along the children in the render function. Here’s a small example:
function Buttons(props) {
const [selected, setSelected] = useState("None")
return (
<>
<h2>The current selected item is {selected}</h2>
{props.children}
</>
)
}
function App() {
return (
<Buttons>
<button value="A">A</button>
<button value="B">B</button>
<button value="C">C</button>
</Buttons>
)
}
What’s the problem here? Well, setSelected
is never called and the buttons don’t have an onClick
! So, even while the buttons are clickable, the state is never set and as a result our current selected remains "None"
.
Two solutions jump out as feasible:
This is the solution I’m most comfortable with. Lifting the state up to the level of the App
component and then managing it Buttons
can become a dumb functional component.
function Buttons(props) {
return (
<>
<h2>The current selected item is {props.selected}</h2>
{props.children}
</>
)
}
export default function App() {
const [selected, setSelected] = useState("None")
return (
<Buttons selected={selected}>
<button onClick={(e) => setSelected(e.target.value)} value="A">
A
</button>
<button onClick={(e) => setSelected(e.target.value)} value="B">
B
</button>
<button onClick={(e) => setSelected(e.target.value)} value="C">
C
</button>
</Buttons>
)
}
There are some drawbacks to this approach though:
Buttons
.Instead of lifting the state, there’s an alternative approach - but it means no longer simply spreading the children props as in the original approach, but cloning them. This is necessary because as Joe Maddalone explains in his EggHead.io video, Use React.cloneElement to Extend Functionality of Children Components, ”props.children
isn’t the actual children. It’s a descriptor of the children…”
Practically, then, we need to clone the descriptor into an actual component which we can then adjust accordingly.
function Buttons(props) {
const [selected, setSelected] = useState("None")
const buttons = props.children.map((child) =>
React.cloneElement(child, {
onClick: (event) => setSelected(event.target.value),
}),
)
return (
<>
<h2>The current selected item is {selected}</h2>
{buttons}
</>
)
}
export default function App() {
return (
<Buttons>
<button value="A">A</button>
<button value="B">B</button>
<button value="C">C</button>
</Buttons>
)
}
In the above example, we took each child and mapped over the list appending an onClick prop to each.
It’s worth noting that this will break if you try to clone an element that’s not a valid React element (e.g., plain text). In that case, you may avoid this by using the isValidElement
top level React API prior to mutation (similarly, you can add any other conditional checks as necessary):
const buttons = props.children.map((child) => {
+ if (!React.isValidElement(child)) {
+ return child
+ }
return React.cloneElement(child, {
onClick: (event) => setSelected(event.target.value),
})
})
The cloneElement
API is not something you’ll likely need to use every day. In fact, as I demonstrated, there’s almost always an alternative approach.
That said, what I like about cloneElement
is how it clarifies the intent and allows related logic to stay together. One of my favorite features of working with React is that it enables the business logic of a component to be co-located with the markup. cloneElement
is just one more tool in the toolbox for enabling that!
Hi there and thanks for reading! My name's Stephen. I live in Chicago with my wife, Kate, and dog, Finn. Want more? See about and get in touch!