Render Props are a React pattern for component composition that can solve essentially the same challenges as higher-order components. While higher-order components are based on higher-order functions, which wrap other functions and can modify arguments, render props are based on callback functions. In many ways, render props are an inversion of the higher-order component pattern, flipping it inside out to inject props inside your component rather than outside. This simplifies many use cases, but it’s also easy to misuse – resulting in a tangled pyramid of callback functions that can be difficult to maintain.
When Props Collide
First, though – let’s talk about the advantages render props have over higher-order components. The biggest, in my mind, is their more explicit nature versus the implicit pass-through nature of higher-order components. This is especially evident if you’re using a strict type system like TypeScript. Consider the overly-simplified component below:
If I want to bring in
react-navigation, I need to use the
withNavigation higher-order component:
This makes my
Props interface implicitly grow with whatever is in
NavigationInjectedProps, and TypeScript must work to ensure that each interface is compatible with each other. When your component is simple like this one, not much of a challenge is presented. Other wrappers are more complex, however. The higher-order components included with
formik both inject a variety of props. The
react-apollo higher-order component injects a single prop that is very complex and relies on a generic type argument.
All of these wrapper components and their props must be merged not just into a single often monolithic
Props interface to be used with your inner component, but also each intermediate step along the way must be checked by the type checker for compatibility. When the type checker can’t unify types on an intermediate step, the error message is often obnoxiously inscrutable:
Render props sidestep this problem by not attempting to intersect the props at all, rather providing a constrained set of local props to a callback function. Consider a slightly different example,
With this approach, rather than injecting the retrieved
collections as an outer prop the user receives the prop as a callback, keeping the scope local to the function. This allows your props to more effectively reflect what consumers of the component need to provide in order to use it, and it makes the type checking much easier and more obvious.
How the Render Prop is Made
Here’s how the
CollectionQuery component used above is put together:
: Collection): ReactNode expression defines a render prop that takes a callback expecting the retrieved collections, and returning something renderable.
One unique feature of render props, however, is that you don’t have to rely on a single
children prop to define a renderable region in your component. You can use multiple render props named whatever you like, giving your consumer flexibility to render a variety of things in a variety of places within your component.
Unicorns & Rainbows Forever!
Hooray! We’re saved! Right? Well, not quite. With the advantages of callback functions come the pitfalls, and you can easily find yourself tangled up in a pyramid of nested inline functions:
To fight this, I embrace a few strategies to keep things under control:
- Use class methods to logically organize callbacks
- Break components up into small, single-purpose pieces wherever practical
- Abstract repeated structures into reusable pieces
Ethical Class-Based Components
If you avoid implicit state and keep things practically pure, class methods can be a clean and obvious way to organize callback functions for things like render props and event handlers.
The event handler takes the piece of data from the
This is only usable in small bites, however. If you pack a single component with a dozen render functions and just as many event handlers, you’ll find yourself or your team scratching your heads trying to pick apart all the things that are happening in one place. Breaking components into smaller pieces helps with readability and maintainability.
Abstracting reusable routines into render-prop components as I did above with
ComponentQuery helps a lot to reduce boilerplate and allow you to take advantage of your effort in more places, without dealing with growing
Props types and convoluted type errors.
Inversion of Control
Another important aspect of render props is the way that they allow for an inversion of control from the callee to the caller. Rather than giving a component an intricate set of configuration props to describe all of the ways that you anticipate the user would want the component to be rendered, you can give the user the power to render whatever they want using the callback function.
Say you have a
CoolWidget that has a
left and a
right region, and you want to allow the user to render their own icon in each spot.
What if the user wants to render their own icon that you don’t support in your library? Or, what if your user wants to render something else entirely? You can either provide an ever-growing set of props to describe an ever-growing set of internal functionality based on them, or you can use callback functions to render whatever the caller gives you.
It doesn’t matter what
this.renderRight are in this case – because they could be anything! As long as you constrain your
right props to return
ReactNode, then you know you’ll be able to render it in the right spot.
The Right Tool for the Job
Render props aren’t a cure-all for tangled higher-order components, and they come with their own whole new set of pitfalls. At their best, however, they can flip the wrapper-component pattern inside-out, making it easier to keep track of props without having to check interlocking sets of intermediate props along a complicated pipeline of wrapping components.
Sometimes, however, higher-order components really are the best tool for the job. Redux’s
connect function remains an outstanding way to attach global state to your components in a controlled way. The
withApollo wrapper is still great for quickly injecting a GraphQL client that you can anywhere inside your component. The more complex your component composition chain is, however, the more opportunities you encounter for confusing prop issues.
As in anything, make sure you’re taking a balanced approach and considering whether you’re using the right tool for the problem you’re solving. With some care, render props can be a well-controlled way to compose components in an obvious way, while providing valuable inversion of control and allowing for easier type checking!