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?

2nd November, 2018

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.

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:

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.

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.

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.

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!

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.

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.

exportclassLinkextendsReact.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.Providervalue={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.

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.

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.

Go Pro

Stay in the loop.

Keeping up to date with React is a full time job that pays only in frustration. Luckily, you can delegate! Just become a free member, and we'll keep you up to date with our monthly newsletter.