Composable Compound Components with `react-call-return`
--
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 option
s, a table
and the nested thead
, tbody
, tr
andtd
tags or an ol
and nested li
s.
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:
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:
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:
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.
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 Li
s: the parent would have to render its children first, in order to know how many Li
s 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.
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 div
s. 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):
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!