24 September, 2019 / Profile
How to pass data to props.children
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.