Photo by Thomas Tastet (Unsplash)
So, for a long time I thought when you have created an instance of an component (an element) you had no way of altering it. This was usually a problem for me when building reusable components.
But there is a React helper method for this!
React.cloneElement
The utility I am referring to is the cloneElement function exposed. You can use this to Clone and return a new React element using element as the starting point as stated on the docs.
The function accepts three arguments (one mandatory)
- The element to clone (This one is mandatory, of course...)
- The props to spread to the cloned element props.
- The new children to append to the element. If omitted the original children will remain.
For example lets override the click event and text on an imaginary Button component:
const buttonElement = (
<button onClick={() => alert('hello')>Click me!</button>
)
React.cloneElement(
buttonElement,
{
onClick: () => alert('This replaced the original onClick prop')
},
"I am the new text"
)
That is all there is to it really. The cloned element will have all the same props but a new click handler. And the children have been replaces with a new text.
Lets build something
The code for this example can be found here
We will build a popup menu with a list of actions. The consumer will only add regular button or anchor elements as children and we will enhance they all with consistent styling and event handlers to open/close the popup.
First just write a little helper. This piece of code will just ensure the children to be an array so we can use map of it
function toArray(items) {
if (!items) return [];
if (Array.isArray(items)) return items;
return [items];
}
Next up, the component. And it is quite straight forward. Simple state hook to handle the open/closed state const [open, setOpen] = useState(false)
.
Somewhere in the component we will alter our child components:
{toArray(children).map((c) =>
React.cloneElement(c,
{
className: "button",
style: undefined,
onClick: function (e) {
setOpen(false);
c.props.onClick?.(e)
}
})
)}
We simply clone the element, override the styles and className property to ensure a consistent styling.
The onClick
metod is enhanced, meaning we add our own implementation that closes the menu but also calls the existing onClick method, if it is defined, using optional chaining (hence the question mark)
The full code for the Menu component:
function Menu({ children }) {
const [open, setOpen] = useState(false);
return (
<div className="button-menu">
<button
className="menu-toggle"
aria-controls="menu"
aria-expanded={open}
onClick={() => setOpen(!open)}
>
{open ? "Close" : "Open"}
</button>
<div
id="menu"
className="button-group"
style={{ display: open ? "inherit" : "none" }}
>
{/*
This is the important part
*/}
{toArray(children).map((c) => {
return React.cloneElement(c, {
className: "button",
style: undefined,
onClick: function (e) {
setOpen(false);
//eslint-disable-next-line
c.props.onClick?.(e);
}
});
})}
</div>
</div>
);
}
The only quirck with this approach is that you need to set keys for the elements inside the Menu component:
export default function App() {
return (
<Menu>
<button key="a"
onClick={() => alert("I am from the button")}
>
I am button
</button>
<a key="b" href="#something">
I am an anchor
</a>
<div key="c">Divs should not pose as buttons...</div>
</Menu>
);
}
Top comments (0)