Compound components is an advanced pattern so it might be overwhelming to use. This guide aims to help you understand the pattern so that you can use it effectively with confidence and clarity.
Note — In this article, we’ll use the new context API introduced in React v16.3. Other articles implement the pattern using
React.cloneElement but that involves cloning each component which increases the memory footprint. It also limits the components to be immediate children of parent. So using context API is much better.
Compound components is a pattern in which components are used together such that they share an implicit state that let’s them communicate with each other in the background.
Think of compound components like the
<option>elements in HTML. Apart they don't do too much, but together they allow you to create the complete experience. — Kent C. Dodds
When you click on an option, select knows which option you clicked. Like select and option, the components share the state on their own, you don’t have to explicitly configure them.
When you click on the buttons inside Tab component the corresponding tab panels’ content is rendered.
Render props is a great pattern. It is very versatile and easy to understand. However this doesn’t mean that we have to use it everywhere. If used carelessly it can lead to obfuscated code.
Having too many nested functions inside the markup lead to difficulty in reading. Remember nothing is a silver bullet, not even render props.
By looking at the example some advantages are pretty clear.
- The developer owns the markup. The implementation of Tab Switcher doesn’t need a fixed markup structure. You can do whatever you like, nest a Tab 10 levels deep (I’m not judging) and it will still work.
- The developer can rearrange the components in any order. Suppose you want the Tabs to come below the Tab Panels. No changes are required in the component implementation, we just have to rearrange the components in the markup.
- The components don’t have to be jammed together explicitly, they can be written independently but they are still able to communicate. In the example Tab and TabPanel components are not connected directly but they are able to communicate via their parent TabSwitcher component.
- The parent component (
TabSwitcher) has some state.
- Using the context-api,
TabSwitchershares it’s state and methods with child components.
- The child component
Tabuses the shared methods to communicate with
- The child component
TabPaneluses the shared state to decide if it should render its content.
To implement a compound component, I usually follow these steps.
- List down the components required.
- Write the boilerplate.
- Implement the individual components.
For the TabSwitcher we need to have two things. One is to know which tab content to show and second is to switch tab panels when user click.
This means we need to control the rendering of tab panel content and have a click event listener on the tabs, so when tab is clicked the corresponding tab panel content is shown.
To accomplish this we need three components.
- TabSwitcher — parent component to hold the state
- Tab — component which tell its parent if its clicked
- TabPanel — component which renders when parent tells it to
The compound component pattern has some boilerplate code. This is great because in most cases we can write it without much thinking.
Here we are making a context. The child components will take data and methods from the context. The data will be the state shared by the parent and the methods will be for communicating changes to the state back to the parent.
The Tab component needs to listen to click events and tell parent which tab was clicked. It can be implemented like this —
Tab component takes id prop and on click event call changeTab method passing its id. This way the parent gets to know which Tab was clicked.
TabPanel component needs to render its children only when it is the active panel. It can be implemented like this —
TabPanel component takes whenActive prop which tells it when to render the children. The context provides the activeTabId through which TabPanel decides if it should render its children or not.
TabSwitcher component needs to maintain active tab state and pass the state and methods to the child components.
TabSwitcher component stores activeTabId, by default it is ‘a’. So the first panel will be visible initially. It has a changeTab method which is used to update the activeTabId state. TabSwitcher shares the state and the methods to the consumers.
Let’s see how they all fit together.
The compound component can be used like this:
LogRocket is a frontend logging tool that lets you replay problems as if they happened in your own browser. Instead of guessing why errors happen, or asking users for screenshots and log dumps, LogRocket lets you replay the session to quickly understand what went wrong. It works perfectly with any app, regardless of framework, and has plugins to log additional context from Redux, Vuex, and @ngrx/store.