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.children.map()
, and React.cloneElement()
. Using these methods, the component is able to express itself in such a way that promotes patterns of composition and extensibility.The Problem:
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.
The Solution:
What if we could express this problem by simply declaring what components we want to be used by passing them to
<ShoppingCart />
directly as children
?
Internally,
<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.
The Code:
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.Children.map()
, React.isValidElement()
, and 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 <ShoppingCart />
.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.Conclusion:
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.
More resources: