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
table and the nested
td tags or an
ol and nested
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’.
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
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
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.
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
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:
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!