One of React’s most useful features is the ability for components to receive and render child elements. It makes it ridiculously easy to create reusable components. Just wrap props.children
with some markup or behavior, and presto.
const Panel = (props) =>
<div className="Panel">
{props.children}
</div>
There is, however, one major limitation to this pattern: props.children
must be rendered as-is. What if you want the component to pass its own props to those children?
For example, what if you’re building a <Link>
component, and you want its children to have access to an active
prop that indicates whether the link points to the current page?
<Link href="/browse/">
<span className={active ? "active" : "inactive"}>Browse</span>
</Link>
Or what if you’re building a <List>
, and you want to automatically add some classes to the components children?
<List>
<Item className="top" />
<Item className="bottom" />
</List>
As it turns out, there are a couple ways to achieve this:
- You can pass a render function to your component in place of its children.
- You can merge new props into the elements passed to
props.children
by cloning them.
Both approaches have their advantages and disadvantages, so let’s go over them one at a time before discussing how they compare.
#Render functions
One of the neat things about JSX is that its elements are just JavaScript objects. This means that a JSX element’s props
and children
can be anything that you could place in a JavaScript variable — they can be strings, other element objects, or even functions.
The simplest way to use a function as an element’s children
is to interpolate an arrow function with JSX. This pattern is often called passing a render function. Here’s an example:
// Display a link, whose style changes depending on whether the
// link points to the window's current location.
<Link href='/browse'>
{isActive => (
<span style={{ color: isActive ? 'red' : 'black' }}>
Browse
</span>
)}
</Link>
Passing data to descendants
While render functions allow components to pass data to their immediate children, React’s context API lets components pass data to deeply nested descendants. In fact, the context API itself uses render functions.
In the above example, the <Link>
component will receive a children
function that expects to be called with a single argument: isActive
. The component can then call the function whenever the user navigates, and then return the result from its own render()
function.
Here’s an example of what this might look like if the <Link>
element receives the window’s current location via context (which itself uses a render function):
export const Link = (props) =>
<LocationContext.Consumer>
{location =>
<a {...props}>
{
// Call the render function to get the `<a>` tag's children.
props.children(location.pathname === props.href)
}
</a>
}
</LocationContext.Consumer>
It can also be called...
The term render function is also used when passing a function to the render
prop — as opposed to the children
prop.
This pattern is also sometimes referred to as a render prop.
Render functions are typically used when the children that are passed to a component may utilize some state that is contained within that component. But render functions aren’t always the perfect solution. In cases where you need to squeeze every last drop of performance out of a component, or when a function just feels wrong, then cloning can come in handy.
#Cloning children
While it’s certainly possible to pass a function as an element’s children
prop, it’s far more common to pass JSX elements. And given that JSX elements are actually plain old JavaScript objects, it follows that the component that receives those elements should be able to pass in data by updating props.
Unfortunately, attempting to set an element’s props
will cause an error.
import React from 'react'
// Create an element object and assign it to `element`
let element = <div>Hello, world!</div>
// Log the value of the `children` prop
console.log(element.props.children)
// Oh no! This doesn't work.
element.props.className = "active"
It turns out that element objects are immutable objects. Once they’ve been created, you can’t change them. But you can clone them — and merge in updated props in the process.
To clone an element, you’ll need to use React’s cloneElement()
function.
React.cloneElement(element, props, ...children)
The cloneElement()
function returns a new element object with the same type
and props
as its first argument, but with the specified props
and children
merged over the top.
For example, here’s how you’d use it to add a className
prop to an existing element.
import React from 'react'
// Create an element object and assign it to `element`
let element = <div>Hello, world!</div>
// Create a clone of the element, adding in a new `className` prop
// in the process.
let elementWithClassName =
React.cloneElement(element, { className: "active" })
// The new element has a `className`!
console.log("new className", elementWithClassName.props.className)
If you’re comfortable with React’s createElement() function, then cloneElement()
will feel familiar — the only difference is that it accepts an element in place of a type
. Of course, given that you won’t use cloneElement()
all that often, you might want some help remembering it. And that’s what my printable React cheatsheet is for — its a free download! But I digress. So let’s move on to an example.
#Cloning props onto children
Suppose that you have a <List>
component, and that you’d like it to add top
and bottom
CSS classes to the first and last children. Let’s also say that when your list only has one child, it should add both classes to the child.
<List>
<div className="top bottom" />
</List>
In the case of a single child, this can be accomplished by cloning it and adding a className
prop with the value top bottom
, as so.
///App.js
import React from 'react'
const List = (props) => (
<div className="List">
{React.cloneElement(props.children, { className: "top bottom" })}
</div>
)
export const App = () =>
<List>
<div>Tame Impala</div>
</List>
///index.js
import React from 'react'
import ReactDOM from 'react-dom'
import { App } from './App.js'
ReactDOM.render(
<App />,
document.getElementById('root')
)
///helper:styles.css
But while this works when <List>
only receives a single child, it fails spectacularly when you you pass multiple children. You can check this for yourself — just add a few more band names to the the list in the above example.
#Manipulating children
Components never know what type of data their children
prop will be. It could be an array, it could be an element, it could be text. It could be anything.
Working with data of type “anything” is never fun. Luckily, React provides some tools to help on its React.Children object. In particular, the React.Children.toArray()
function will come in handy for this example.
// Always returns an array.
React.Children.toArray(children)
The React.Children.toArray()
function does exactly what you’d imagine it does. You pass it a children
prop, and it’ll return an array. If children
only contains one child, it’ll wrap that child in an array, which you can then slice()
and concat()
like a ninja.
To put this all together, here’s how you’d use React.Children.toArray()
with React.cloneElement()
to add top
and bottom
classes to the List component’s children.
///App.js
import React from 'react'
const List = (props) => {
let elements = React.Children.toArray(props.children)
if (elements.length === 1) {
elements = React.cloneElement(elements[0], { className: 'top bottom' })
}
else if (elements.length > 0) {
let lastElement = elements[elements.length - 1]
elements =
[React.cloneElement(elements[0], { className: 'top' })]
.concat(elements.slice(1, -1))
.concat(React.cloneElement(lastElement, { className: 'bottom' }))
}
return (
<div className="List">
{elements}
</div>
)
}
export const App = () =>
<List>
<div>Tame Impala</div>
<div>Kingswood</div>
<div>Flight Facilities</div>
</List>
///index.js
import React from 'react'
import ReactDOM from 'react-dom'
import { App } from './App.js'
ReactDOM.render(
<App />,
document.getElementById('root')
)
///helper:styles.css
The above example might not be pretty, but it works… most of the time.
#Edge cases
One of the problems with cloning children is that it can be pretty easy to get wrong. There are a lot of edge cases, and unlike with render functions, you’ll need to take account of all of those edge cases within the component that does the cloning.
For example, if you were to add a className
prop to the top or bottom <div>
in the above demo, the <List>
component would remove it in place of the top
or bottom
class. You can see this in the following example, where I’ve added the highlight
class to the top two rows, but only the middle row is actually highlighted.
To fix this, you’ll need to append a string to the className
instead of replacing it. In fact, let’s do this as an exercise!
Your task is to ensure that the top and bottom elements of the list render with both their original class, as well as a top
or bottom
class.
Hint: as React.cloneElement()
only knows how to replace props, you’ll need to figure out how to build the new string by inspecting the original element.
///App.js
import React from 'react'
const List = (props) => {
let elements = React.Children.toArray(props.children)
if (elements.length === 1) {
elements = React.cloneElement(elements[0], { className: 'top bottom' })
}
else if (elements.length > 0) {
let lastElement = elements[elements.length - 1]
elements =
[React.cloneElement(elements[0], { className: 'top' })]
.concat(elements.slice(1, -1))
.concat(React.cloneElement(lastElement, { className: 'bottom' }))
}
return (
<div className="List">
{elements}
</div>
)
}
export const App = () =>
<List>
<div className="highlight">Tame Impala</div>
<div className="highlight">Kingswood</div>
<div>Flight Facilities</div>
</List>
///solution:App.js
import React from 'react'
const List = (props) => {
let elements = React.Children.toArray(props.children)
if (elements.length === 1) {
let className = (elements[0].props.className || '') + ' top bottom'
elements = React.cloneElement(elements[0], { className })
}
else if (elements.length > 0) {
let lastElement = elements[elements.length - 1]
let firstClassName = (elements[0].props.className || '') + ' top'
let lastClassName = (lastElement.props.className || '') + ' bottom'
elements =
[React.cloneElement(elements[0], { className: firstClassName })]
.concat(elements.slice(1, -1))
.concat(React.cloneElement(lastElement, { className: lastClassName }))
}
return (
<div className="List">
{elements}
</div>
)
}
export const App = () =>
<List>
<div className="highlight">Tame Impala</div>
<div className="highlight">Kingswood</div>
<div>Flight Facilities</div>
</List>
///index.js
import React from 'react'
import ReactDOM from 'react-dom'
import { App } from './App.js'
ReactDOM.render(
<App />,
document.getElementById('root')
)
///helper:styles.css
How’d you go? If you got stuck on this exercise, head on over to my React fundamentals course. It’ll leave you as a master of React’s element objects, and you can get started immediately!
#Which approach should I take?
The render function approach has recently grown in popularity. In fact, React itself now makes use of render props in its own context API, and provides documentation on the official website.
But why have render functions become so popular? There’s a couple reasons:
- Render functions make it clear that data is being added to your children, while cloning hides the additional props within another component’s implementation.
- As arrow functions have become more common in application code, render functions are becoming less exotic and easier to understand at first glance.
Of course, there are still situations in which cloning makes sense:
- If you want to add props to a list of child elements, cloning will probably be more readable than creating a list of render functions.
- When you need to squeeze every last drop of performance out of your code, cloning can be a lot faster than render functions.
So which should you use?
My recommendation is to use render props where possible. But you don’t have to avoid cloneElement()
completely — its still a useful and effective tool when used in moderation.
Thanks so much for reading! If you’ve found this helpful, take a look at my complete React fundamentals course. It’s packed full of live examples and exercises, and will help deepen your understanding of React’s fundamentals, including:
- elements
- function components
- events
- hooks
- class components
- and more
You can try out the first lessons and exercises for free — see you there!