React’s Render Props in Practice
Taking render props from theory into practice
If you are a front-end developer using ReactJS, you’ve probably caught a lot of buzz about render props and how they are supposed to be this great solution for situations where your components become too complex to work with, but like me, never have realized how it comes to play in real life.
In the following story I will walk you through the phases of understanding how the need for render props arises, and when it does, how to make them work for you.
ReactJS is all about components, and R&D organizations which adopt ReactJS as their client side technology very quickly find themselves developing their own components library for common usage bearing many custom-made components.
Such was the Pager component which we found ourselves discussing one fine afternoon in a meeting. A component containing Prev/Next arrow buttons which shifted the current page cursor and dispatched a “change” event to notify its surrounding components, but alas, now a new functionality and appearance requirements raised questions about the nature of this component.
The new version of the Pager component should include quick-paging buttons which allow you to jump to a certain page within a buffer of n pages, while still maintaining the old arrow buttons’ functionality.
Check out the images below:
How would you approach it?
One of the suggestions was to add a variant to the component (a “prop” to the rest of the React world), which will determine the appearance and functionality we desire out of the existing component, while the other suggestion was to build a new component, since the implementation of the new requirements within the existing Pager component would make it a nightmare to maintain, and bug-prone due to high complexity.
Kent C. Dodds is one of my favorite React mavens. His ReactJS courses are very popular and offer a smooth yet comprehensive introduction to the React world.
A recent talk he gave in “React Rally 2018” was published and I took the 25 minutes to sit and watch it. This talk was called “Simply React”, and delivered content that could not be far from it.
Briefly put, he told the story of a poor React component developer who needed to keep extending her component’s functionality and appearance to a complete complexity and props mess, with the amazing twist at the end (spoiler alert!) — all the live demos for each enhancement presented were based on a single, 37 lines of code, component (the link to the presentation can be found at the bottom of this story). Wow!
This is exactly what we needed! A single, simple to maintain component that could enable us to easily introduce new functionality and appearances to it. A component that can support both “this and that” without bloating its props. Can we convert the Pager component into a single base-component that can be extended and composed to achieve our goals?
Getting to Work
I assume that most of you reading the piece know how to implement a component which renders and acts differently according to a prop or multiple props, so I am not going to show examples of that, but rather, jump straight to the road which led me to the solution’s implementation.
Before we start, I’ll preface this by saying that the focus of the following code examples is on the core concepts brought in this story. You won’t see any fancy style references and some functionalities/validations will only be hinted in the code.
The Naive Start
You first start with the naive implementation of the Pager component which will receive a pagesCount, a cursor, prev/next button labels, and an onPageChange callback that will receive the current cursor as an argument. When we reach either the low or the high page limit the “prev” and “next” buttons become disabled accordingly. Let’s see how the component looks:
(By the way, declaring the state as I did is made possible with this babel plugin)
In order to use it we will do the following:
Now we’ve got a component which does what we want, but that’s about it and nothing more. The only way to alter its appearance without touching its core code is by changing the text which appears on the arrow buttons. We can’t even change the “pipe” separator if we want to. We need more.
The “Compound Components” Approach
We’ve gotten the basics going, but we would like to give more freedom to the look and layout of the component (e.g., have the prev and next buttons be actual buttons, or maybe change the separator, while keeping the same layout and logic). The mere button text props from the Naive Pager component will not be enough.
This is where “Compound Components” comes in handy, basically declaring placeholders for the next/prev buttons which encapsulate the interaction logic of the Pager component. These are “inner” components inside our Pager component which we can later use when composing our final presentation.
Our compound components are named “Prev” and “Next” and both can get props of their own and render their children accordingly. Their children are what we would like to appear instead of just hard-coded text.
The way the compound component interacts with the “parent” component props is via the approved React Context API. This way we are able to provide nested DOM structure as our Next/Prev children which can still access the “parent” component’s state and props. In other words, the component’s context wraps its children’s rendering and gives them access to the cursor and pagesCount props plus the changePage method, all declared and managed in the “parent” component.
Notice that in this example I’m rendering “N/A” when the cursor reaches max or min, but that is just to emphasize that the Next and Prev compound components can act according to the cursor position. Let’s see how the component looks:
In order to use it we will do the following:
Now we have more control over the component’s appearance — we changed the Prev/Next into buttons and the separator from a pipe to a hyphen. We can even change the order and layout of these buttons using plain JSX and CSS while not risking the component’s functionality. Brilliant.
… but still not enough.
We cannot settle with having only compound components, because we need to actually change the building blocks which make the component. In addition to the arrows we need “page-buttons” in the middle, for quick paging.
Now we’re ditching the compound components and we’re going for render props.
A component with a render prop takes a function that returns a React element and calls it instead of implementing its own render logic.
In short, render props hand off the composition responsibility to the one using the component, which means that our basic Pager component should expose its API (state and callbacks) to be consumed by other composed components that are more focused on the desired appearance and functionality.
Our core functionality stays the same (pages, cursor, and event), but now there is a different appearance and interaction with the component which wraps the basic functionality. First, let’s see our basic Pager component and understand what’s going on there:
The interesting part in the basic component above is the render method. We see that we’re returning the invoke result of the component’s children, so we understand that the children of the PagerBasic should actually be a function, and this function receives 5 arguments — the cursor, the pagesCount, callbacks for prev/next, and changePage. You can see that the changePage implementation stays the same as within the previous component examples above.
Now that we have this basic component, how do we proceed? Let’s first attempt to implement our simple prev/next component using the PagerBasic component.
In the example above we’re declaring a functional component called PrevNextPager that uses the PagerBasic component and supplies the render function (i.e., the render props) to it as children.
Inside the render function we construct how we would like our component to look, relying on the arguments the function receives (remember the PagerBasic render method?). The state and logic of the component are kept intact within the PagerBasic component. In the PrevNextPager we simply supply the means to display and interact with it.
As you can see, using it is very simple. Give the component the props it requires and you have it! Now you can decide whether this should be a component declared in a separate file, or in the file using it, or even inline the JSX, it’s your call according to how you see the composed component usage in the future. Let it sink for a sec… pretty neat, right?
It’s time to deal with the more complex component. We start again by composing our basic component, but now we inject the page navigation part (the page navigation can even be a component on its own, but in order to keep things clearer I’ve implemented it inline). For this component we need to add an extra prop to support a buffer count which determines how many pages will be displayed between the arrows (we don’t have to, but why the hell not). We’ll add this prop to our new component.
This is the result:
Don’t be intimidated by the QuickPager render props implementation. It simply takes the buffer prop and creates those page buttons while assigning them the page index they represent. A click on each will call the changePage method (which is passed as an argument) with the desired page index. From there on, it’s all the PagerBasic’s job to take care of. I even marked the selected page with a corresponding className just to show that you can manage this selection outside the BasicPager. The BasicPager tells you which is the current selected page index. It’s your responsibility to do with the data as you wish.
It is also important to notice how we only added what’s needed where it is needed, i.e., the buffer prop. Other components relying on our BasicPager will not be affected by it. This, in turn, also helps keep our new component easy to maintain.
This is some powerful stuff, right? But you know what they say — with great power comes great responsibility, so I won’t advise you to go and change all your components to support render props. Having said that, I’m sure you can think of several components on your repository which can benefit greatly from this approach.
I’m happy to hear your thoughts on this story, so feel free to leave them here. You can also find me on Twitter @mattibarzeev.
Happy coding ☺