React.js — Compound Components
A compound component is a type of component that manages the internal state of a feature while delegating control of the rendering to the place of implementation opposed to the point of declaration. They provide a way to shield feature specific logic from the rest of the app providing a clean and expressive API for consuming the component. Internally they are built to operate on a set of data that is passed in through
children instead of
props. Behind the scenes they make use of React’s lower level API such as
React.cloneElement(). Using these methods, the component is able to express itself in such a way that promotes patterns of composition and extensibility.
Let’s say we are tasked with creating a shopping cart component that displays a grouping of items in either a horizontal or vertical list. Each item must be assigned a click handler to capture click events and have an
isActive state conditionally applied to it.
A common approach would be to design your component to take a list of items as a prop and have the component internally
.map() over them:
This is a fairly common approach that provides one possible solution to our problem. There is however, an inherit downside to this pattern:
- The responsibility for rending
<ShoppingCartItem />is held by
<ShoppingCart />which causes a coupling between the two components. This takes away the aspect of composition as it forces
<ShoppingCart />to know about which components should be used for each item.
What if we could express this problem by simply declaring what components we want to be used by passing them to
<ShoppingCart /> directly as
<ShoppingCart /> maps over the
children cloning the click handler and
direction prop onto each item.
If we later decide that certain items should use a different click handler, we can simply override the
onClick passed in to
<ShoppingCart /> by specifying the exception directly on the item that requires differentiation:
On top of the immediate improvement to readability, we start to see some of the benefits of composition.
Let’s say we have an added requirement to display one of the item’s as an expandable card. With our new implementation, it’s as simple as passing the new component as an additional child:
Because no assumptions have been made by
<ShoppingCart /> about which components should be rendered, so long as the children passed to it are built to accept the same props, everything will continue to work.
Let’s take a deeper look at what’s going on beneath the hood.
As you saw in the previous examples, we have updated
<ShoppingCart /> to take the cart items as
children instead of as a regular
prop. To support this change, our new ShoppingCart.js file now looks as follows:
The first things you will notice are the usage of
React.cloneElement() in our
renderItems method. The second major change is that we now read the list of items through
this.props.children instead of
this.props.items. In it’s simplest form,
children is really just another way to pass props. It just so happens that it’s a more intuitive way to pass JSX which is why it’s used here.
Let’s break things down further.
First, we use
React.Children.map() to loop over each child:
We use this method instead of a native map because
React.Children.map() knows how to handle cases when something other than a React element is passed. Remember how
children is basically just another way to pass props? It can also be used to pass things like strings, and functions in which case calling a native map on it would cause an error:
Next, we use
React.isValidElement(child) to check if the item from the current iteration of the map is in fact a valid React element:
This will guard against cases when something like a
string or a
number is passed.
Finally, we use
React.cloneElement(child, [props]) to create a copy of the element, injecting additional feature specific props from
<ShoppingCart /> into the clone:
In our example, we use the
index from the
React.Children.map() method to determine whether the child is the active item. We also apply a custom
onClick handler which sets the new
activeItemIndex to the index of whatever child was clicked. The click handler then does a check to see if the child was explicitly passed an
onClick handler of it’s own, in which case we then proceed to call it to propagate the event. Otherwise we set it to trigger the default handler passed in as a prop to
Putting all the pieces together, what we are left with is an array of cloned children that have feature specific props from
<ShoppingCart /> injected into them:
Compound components provide a powerful way to create reusable, consumer centric React components. They give developers a way to separate the business logic for a feature from the rendering logic which ultimately leads to a cleaner and more expressive component API.
For more information about compound components and an example of them in action, see the sections below.