Composable Compound Components with `react-call-return`

Reinier Hartog
Apr 20, 2018 · 4 min read

WARNING: This article discusses an unstable API in React. This means the API likely still contains bugs, and can be changed or removed entirely in any minor upgrade of React. Use only in experiments for now!
EDIT: This feature was removed in React 16.4.

Over the last two years, I’ve fallen in love with React. The simplicity of the component model and how everything fits together just clicks with me. However, there is one pattern in particular where I think React is currently lacking: Compound Components.

I believe the term ‘Compound Components’ was first coined by the folks over at React Training. It is used to describe a few components that work together to implement a single control. This idea is not new at all: HTML has had these built-in components for a long time. You can think of a select and its options, a table and the nested thead, tbody, tr andtd tags or an ol and nested lis.

Let’s take that last one as an example: a list of numbered items. To refresh your memory, this is what it looks like using the built-in HTML elements:

An example of a list in HTML.

Simple, right? Every li inside the ol gets a index rendered in front of its ‘children’.

Now imagine this functionality was not available in HTML. Or you want to have more control in JavaScript. Can you recreate this behavior in two React components, Ol and Li? Where would you get the ‘index’ from?

The common solution for creating such Compound Components is for the parent to introspect the ‘children’ prop that is passed to it. To do that, we can call React.Children.map on the children. For each child, we can then use React.cloneElement to take the element that was passed in and give it its index as an additional prop, so the end-user doesn’t have to:

An example in React using React.Children.map and React.cloneElement.

Great! Close enough, right? Some small tweaks in styling and we’re ready to go. Or are we?

When teaching this pattern, this is generally as far as we go. However, there is one major downside to this approach: the Li elements must be passed in directly as children, which inhibits composition.

What am I talking about? Well, for me, the power of React is that I can put any component anywhere and expect it to work together. This ability to compose is what opens up incredible potential. Let’s revert to HTML’s ol and li and try out an example.

I’ve created a Random component that generates a random number between 0 and 5, calls its render prop with that number and renders whatever that returns. Great, now we have an easy way of putting random numbers anywhere, such as in our list:

A list in HTML composed with the Random component.

Or, we could generate a random number of additional items with that same component:

This works fine with the HTML elements, but as you’re probably expecting by now, doing the same for our own components will not work! The Ol will find a Random element in its children and pass the index to it, but that doesn’t do us any good.

Composing our own list implementation with the Random component: not working!

The idea of letting the Ol read its children is flawed if you intend to pass anything other than Li elements. Any of its children could render zero, one or many Lis: the parent would have to render its children first, in order to know how many Lis there will be.

But, fear not, there’s an (unstable) API for that! Remember the warning above though, use this only in experiments for now.

The experimental react-call-return package introduces two new elements: the Call and the Return. Whenever a Call is rendered, it will continue rendering its children, until each of them eventually renders a Return (or nothing). The value of each return is then collected and passed to the Call’s handler, which can then use these values to render whatever it wants.

Let’s rewrite our list implementation using this package: Ol renders a Call, collects all Returns and maps them into a numbered list.

Using Call and Return makes the List implementation composable.

After rewriting the list implementation to use Call and Return, we can see that is starts working with the Random component as well. We’ve regained some of the compositional flexibility we had with the HTML implementation, and are more free to put components together in different ways. One thing to note, though, is that all children of the Call can not render any ‘host’ elements, such as divs. React will throw an error if you do.

We’ve used this new API to create a Compound Component, built out of two parts: Ol and Li, with the goal of hiding the communication between the two from the user of these components. The experimental Call and Return API allows us to make that work, without limiting the way these components can be composed.

There is, however, one more hurdle to overcome in order to make this abstraction truly invisible from the user, and that is related to Context. When new context is provided anywhere between the Call and the Return, this context is not available in the handler of the Call. This can lead to a very confusing situation if the user does not know Call and Return and being used (which we don’t want the user to care about):

Difference in HTML versus the Call and Return implementation regarding context.

I’ve started a discussion on GitHub about this problem, please share your thoughts if you have any. Hopefully, this will lead to a better API for building Compound Components.

So that is that: while it is still unstable and there is work to be done to finalize the API and implementation, react-call-return is a promising API for developers and library authors to create better abstractions over components that have many moving parts.

Have any questions? Comment below or reach out to me on Twitter!

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store