How to master advanced React design patterns: Render Props
In Part 1 of this series we discussed how we could use compound components and static class properties to enable us to produce a readable and reusable Stepper component. We saw that this design pattern has some limitations in that it’s not very flexible; components need to be a direct child of a parent component otherwise it breaks.
In Part 2 we used the new context API to provide an elegant and readable solution to Part 1’s limitations. The problem with this design pattern is that it requires some initial set up for it to work and our component cannot be placed into another application and work independently with out the initial boilerplate code.
In this Part, we will discuss a design pattern that can address all of the problems that we have identified up to this point. It is called: render props.
This design pattern can be a bit of a head scratcher at first (remember the function we had to place inside the context consumer in Part 2?) and in order to truly grasp how it exactly works, we need a in-depth understanding of the top level React API and how the JSX code we write is converted to javascript. So let’s use a very simple example and walk through what is going on under the hood.
JSX
JSX is a syntax extension to JavaScript designed by Facebook’s engineers. We use it with React to describe what the UI should look like (a bit like a template language), but it comes with the full power of JavaScript. Whenever you write any components in JSX, Babel compiles it down to a React.createElement() call.
Let’s look at a very simple example:
The two examples above yield identical results, the parent component is simply converted to a React.createElement() call, the type is our ‘Parent’ component, there are no props and there are no children.
When we add a child component, notice how it is itself converted to a React.createElement() call and it is this format that creates our React component tree.
The key thing to understand here is that Babel compiles down any props added as a single props javascript object; because it is pure javascript we can pass anything we want, such as functions.
In the above example, instead of passing down the ‘string’, we’ve passed down a function that returns the ‘string’. So, when that function is called we can get the exact same result.
So what exactly is going on in the above examples? In the initial example, we just pass down the string, place it in a ‘div’ and it is rendered. In the next example however, we are passing it as a function and placing it in a ‘div’ but this time calling the function allowing us to achieve the exact same result.
Render Props
Why is this important? Well, traditionally we have rendered the children components that we place inside of our parent component.
This is the key thing to understand, instead of designing our components to render a child, there is nothing stopping us from rendering the props instead while achieving the exact same result:
So, in this design pattern we render props not the children. We can take this a step further too. What else can we do with functions? We can pass arguments when we call them:
Let’s take a moment to digest what’s just happened here. We’ve passed in a function like before but instead of always returning ‘string’ it returns the argument we pass in when it is called!
Wait a second, wasn’t this a problem we encountered in Part 1? To resolve it we had to clone the element, loop through each one and then pass down any desired props.
Using this design pattern we are able to pass props down to child components, Boom!
We can name the props anything we want. So instead of using ‘example’, let’s use something more appropriate:
If you have used react router before, this may look very familiar. When you need to pass down props to a route you need to use a render function.
This is render props. Instead of rendering the component directly, we are able to call ‘render’ and pass in any arguments we want.
Let’s swing back to our Stepper component and see how we can utilize this design pattern (I’ve removed all the context boilerplate and added state back to the stepper component).
This time instead of adding {this.props.children} we instead add {this.props.render(stage,HandleClick)} . We no longer need to add any children to the stepper component, all we need to do is return the same markup in the render prop.
What does this achieve? Well, every component in the tree now has access to all the props. It essentially gives us the same exposure to the props as the context API, we don’t have to manually pass the props down to each child and we have the flexibility to move things around. This simple adjustment to component design addresses all of the problems we’ve previously mentioned.
There is one small tradeoff using this design pattern though. The code is slightly less readable than before. Remember that strange function we saw earlier in this series, we needed to add a function inside the Context.consumer component.
This looks very readable to me; let’s think about what is going on. Instead of adding a render function, we are simply adding the same function as a child instead.
Let’s try doing this with our example component we used earlier:
On the left hand side, we are adding the function to the render prop like before. When this is compiled by Babel, the function is added into the the second argument: the props. On the right hand side, we added it as a child and when that is compiled it is added to the third argument: children.
How do we access the children when creating our components?
props.children
In a similar fashion to calling the render prop, as the child is a function we can call props.children instead and pass in our required arguments, giving us the same outcome as before with an enhanced level of readability.
So there you have it, we have designed a component that is highly flexible and extremely readable. Users can have the autonomy to rearrange the child components without worrying if it will have access to the props they need. Ultimately, it is reusable. We can place this directly in any other application without any prior setup and it will work perfectly.
https://medium.com/media/b1a39a8c067cf87b6a0f1bce3ae8545e/href
Top comments (0)