24 September, 2019 / Profile

How to pass data to props.children

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.