Passing state to render props via context

Headless components are a great new way to separate presentation and control logic. But what if you don't want to pick and place all the render function's props manually?

James K Nelson

Hi! I've been playing with JavaScript for over half my life, and am building Frontend Armory to help share what I've learned along the way!

Read more by James

So you’ve probably noticed the recent rise of headless components – i.e. components that facilitate the reuse of control logic by delegating their presentation to a render prop.

There’s a good reason that headless components have become so popular - they’re super practical. And context makes them even better, because it solves one weakness that can make you pull your hair out…

If you don’t mind feeling frustrated for a very short moment, take a look at this headless <Link> component. Its render function’s props object provides all of the state that you need to render the <a> tag. But it still leaves you with the task of picking and placing that state onto the <a> tag, as if you were some kind of factory robot.

<Link href="/browse" render={props =>
  <a {???} style={getStyle(props.active)}>
    Browse
  </a>
}>

Of course, there are ways of getting around this. One common approach is to provide an aProps prop, which can be spread onto the <a> tag:

<Link href="/browse" render={props =>
  <a {...props.aProps} style={getStyle(props.active)}>
    Browse
  </a>
}>

But this has it’s own issues:

  • This pattern works when your headless component should render a single <a> tag, but what if your render function should render multiple <a> tags? Things rapidly get confusing.
  • What if aProps contains a style prop? The render function would need to manually merge it in, creating a lot of work, not to mention opening the possibility of buggy code that fails to do so.
  • The aProps object will certainly have some events handlers – but which event handlers? Not knowing means that merging any of your own handlers in will be a PITA.

And that’s all before I mention the most problematic issue of all:

  • It doesn’t truly separate the concerns. The render function shouldn’t need to know that the <Link> control requires an <a> tag with specific props!

True separation of concerns #

In a happy dreamy world, the render() function would have access to some sort of <Anchor> component that can be used without knowledge of its internals. This <Anchor> component would know how to merge in any styles, handlers, etc. And it might look something like this:

<Link href="/browse" render={props =>
  <Link.Anchor style={getStyle(props.active)}>
    Browse
  </Link.Anchor>
}>

In fact, this API is not only possible (thanks to context) – it’s also already available in a package that I’ll be announcing next week(ish). If you’d like to try it, join Frontend Armory to stay in the loop! But it seems that I’ve gone on a bit of a tangent… so let’s get back on topic and take a look at how you’d build this <Link> component by yourself.

Compound headless components, with React Context #

To implement this design, you’re first going to need some components:

  • You’ll (obviously) need a <Link> component.
  • You’ll also need a <Link.Anchor> component. I’d usually call this component <LinkAnchor>, and then just assign it to a static Anchor property on the <Link> component.
  • Finally, you’ll need a Context object to pass data between the <Link> and <LinkAnchor> components.

Let’s go through these in more detail, starting with the Context object.

1. Create a context #

Creating a context is simple. Just call React.createContext().

const LinkContext = React.createContext({
  href: '',
  onClick: () => {},
})

The object I’ve passed into createContext contains the default value that Consumers will use if no Provider is available. Of course, this should never happen for a <Link> component. But nonetheless, it’s a great way to document the context’s expected value.

2. Create a child component #

The <LinkAnchor> component has two jobs. First, it needs to pull its parent <Link> component’s href and onClick out of context. Then, it needs to merge that state with any props passed in from the render function itself, and apply them to the <a> tag.

Here’s what this might look like in practice:

This component would probably look pettier with React’s proposed useContext() hook.

If you’re looking for a way to try hooks, try refactoring the <LinkAnchor> component in the live editor at the bottom of this page. The editor uses an alpha version of React, so hooks will work – just don’t try this in production!

const LinkAnchor = props => (
  <LinkContext.Consumer>
    {linkContext =>
      <a
        // Spread any props passed to `<Link.Anchor>` onto the `<a>`
        {...props}

        // Set the `href` from context 
        href={linkContext.href}

        // Call *both* onClick handlers, if they exist
        onClick={event => {
          if (props.onClick) {
            props.onClick(event)
            if (!event.defaultPrevented) {
              linkContext.onClick(event)
            }
          }
        }}
      />
    }
  </LinkContext.Consumer>
)

You might have noticed the long-winded onClick handler in the above example – what’s with that? The comment gives you a clue: if props.onClick and linkContext.onClick both exist, then they both need to be called – unless the first caller calls event.preventDefault(). And that’s why I only call linkContext.onClick if event.defaultPrevented isn’t true.

3. Tie everything together #

Now that you have a <LinkAnchor> component to render the actual <a> tag, all that <Link> needs to do is call the render function, and set up the context so that <LinkAnchor> has access to the correct state.

export class Link extends React.Component {
  // Exporting `LinkAnchor` as a static variable on `<Link>` makes it clear
  // that `LinkAnchor` is only meant to be used in conjunction with `<Link>`.
  static Anchor = LinkAnchor

  render() {
    // This should contain any state that is needed to render the actual
    // `<a>` tag.
    let linkContext = {
      href: this.props.href,
      onClick: this.onClick,
    }

    // This should contain any props that the render function needs to
    // handle presentation.
    let rendererProps = {
      active: this.props.href === window.location.pathname
    }

    // The `<LinkContext.Provider>` passes `linkContext` to the
    // `<LinkContext.Consumer>` that is used in `<Link.Anchor>`.
    return (
      <LinkContext.Provider value={linkContext}>
        {this.props.render(rendererProps)}
      </LinkContext.Provider>
    )
  }

  onClick = (event) => {
    window.location = this.props.href
  }
}

Simple, huh? In fact, this example is a little too simple; other than the active boolean that is passed to the render function, this <Link> component doesn’t really provide any extra features compared to a plain old <a> tag. But despite the simplicity, there’s something really cool going on.

As it happens, the <Link> component that drives Frontend Armory has an identical API to this component. Of course, Frontend Armory’s <Link> has many more features – but any render function that you write for the simple example above will also work for the full featured <Link>.

And that’s the beauty of context and headless components - they make separating presentation from logic that much easier.

A real-world example #

To finish off, it often helps to see how concepts are used in the real world. So here’s a live editor with the full featured <Link> component that drives Frontend Armory – in all of its not-cleaned-up-for-publication glory.

Just like the above example, this <Link> is a headless component that passes state to a <Link.Anchor> component via context. But unlike the above component, it has a default render prop that allows it to be used as a plain old <a> tag – which makes it perfect for use with MDX.

This might be a little easier to read if you put the editor into fullscreen with the button at its top right.

Link.js
App.js
main.js
index.html
New
Build In Progress

    There’s a lot going on in this <Link> that is out of the scope of this lesson. So if you’re interested in hearing more about routing and context, create a free account to get the monthly newsletter and stay in the loop!

    Thanks so much for reading – I hope it’s been helpful! If you have any questions or comments, or just want to discuss routing, get in touch by tweeting at @james_k_nelson, or sending an e-mail to james@frontarm.com.

    Finally, I want to say thank you to Adam Rackis, whose tweet triggered a discussion on how to pass state to render props, and to Dan Abramov, who suggested the new context API as a solution. I had another (messier) way of accomplishing this before context arrived, but context really is the perfect way to do it.