Aditya Agarwal Loves experimenting on the web. You can follow me on Twitter @hackerrank.

Understanding React compound components

5 min read 1436

understanding react compound components

Editor’s Note: This post was reviewed and updated on 5 November 2021 with relevant information and code blocks.

Compound components are 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.  In this article, we’ll use context API.

What are compound components in React?

Compound components are a pattern in which components are used together such that they share an implicit state that lets them communicate with each other in the background.

In other words, when multiple components work together to have a shared state and handle logic together, they are called compound components.

Think of compound components like the <select> and <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, so you don’t have to explicitly configure them.

Example of a compound component

<TabSwitcher>
  <header>
    <ul className="tablist">
      <li>
        <Tab id="a">
          <button>Tab A</button>
        </Tab>
      </li>
      <li>
        <Tab id="b">
          <button>Tab B</button>
        </Tab>
      </li>
    </ul>
  </header>
  <main>
    <TabPanel whenActive="a">
      <div>
        a panel
      </div>
    </TabPanel>

    <TabPanel whenActive="b">
      <div>
        b panel
      </div>
    </TabPanel>
  </main>
</TabSwitcher>

When you click on the buttons inside the Tab component, the corresponding tab panels’ content is rendered. Furthermore, notice that we are using multiple components together to create one single compound component. As a result, this helps in code reusability.

Why do I need it when I have render props?

Render props is a great pattern. It is 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 makes it difficult to read. Remember, nothing is a silver bullet, not even render props.

Advantages of using compound component patterns

By looking at the example, some advantages to using compound components are pretty clear.

For example, the developer owns the markup. The implementation of TabSwitcher 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. Because they are compounded together, they can share state data with each other seamlessly

We made a custom demo for .
No really. Click here to check it out.

Also, 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

And finally, components don’t have to be jammed together explicitly. They can be written independently but 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.

How compound components work

  • The parent component (TabSwitcher) has some state
  • Using the context-api, TabSwitcher shares its state and methods with child components. In this case, our child components are Tab and TabPanel
  • The child component Tab uses the shared methods to communicate with TabSwitcher
  • The child component TabPanel uses the shared state to decide if it should render its content

Implementing the TabSwitcher compound component in React

To implement a compound component, I usually follow these steps.

  1. List down the components required
  2. Write the boilerplate
  3. Implement the individual components

List down the components required

For TabSwitcher, we need to have two things. First, you need to know which tab content to show, and second, it should switch tab panels when the user clicks.

This means we need to control the rendering of the 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:

  1. TabSwitcher :  parent component to hold the state
  2. Tab :  component that tell its parent if it’s clicked
  3. TabPanel: component that renders when the parent tells it to

Write the boilerplate

The compound component pattern has some boilerplate code. This is great because in most cases, we can write it without too much thinking.

import React, { useState, createContext, useContext } from "react";

//the name of this context will be DataContext
const DataContext = createContext({});

function Tab({ id, children }) {
  //extract the 'setActiveTabID` method from the DataContext state.
  const [, setActiveTabID] = useContext(DataContext);
  return (
    <div>
      <div onClick={() => setActiveTabID(id)}>{children}</div>
    </div>
  );
}
function TabPanel({ whenActive, children }) {
  //get the 'activeTabID' state from DataContext.
  const [activeTabID] = useContext(DataContext);
  return <div>{activeTabID === whenActive ? children : null}</div>;
}

function TabSwitcher(props) {
  const [activeTabID, setActiveTabID] = useState("a");
  //since this component will provide data to the child components, we will use DataContext.Provider
  return (
    <DataContext.Provider value={[activeTabID, setActiveTabID]}>
      {props.children}
    </DataContext.Provider>
  );
}

export default TabSwitcher;
export { Tab, TabPanel };

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.

Implement the individual components

The Tab component needs to listen to click events and tell the parent which tab was clicked. It can be implemented like this :

function Tab({ id, children }) {
  //extract the 'setActiveTabID` method from the DataContext state.
  const [, setActiveTabID] = useContext(DataContext);
  return (
    <div>
      <div onClick={() => setActiveTabID(id)}>{children}</div>
    </div>
  );
}

Tab component takes the id prop and on-click event call setActiveTabID method, passing its id. This way, the parent knows which Tab was clicked.

The TabPanel component needs to render its children only when it is the active panel. It can be implemented like this :

function TabPanel({ whenActive, children }) {
  //get the 'activeTabID' state from DataContext.
  const [activeTabID] = useContext(DataContext);
  return <div>{activeTabID === whenActive ? children : null}</div>;
}

TabPanel takes whenActiveprop, 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 needs to maintain active tab state and pass the state and methods to the child components.

function TabSwitcher(props) {
  const [activeTabID, setActiveTabID] = useState("a");
  //since this component will provide data to the child components, we will use DataContext.Provider
  return (
    <DataContext.Provider value={[activeTabID, setActiveTabID]}>
      {props.children}
    </DataContext.Provider>
  );
}

The TabSwitcher component stores activeTabID. By default, it is a. So, the first panel will be visible initially. It has a method that 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.

import React, { useState, createContext, useContext } from "react";

//the name of this context will be DataContext
const DataContext = createContext({});

function Tab({ id, children }) {
  //extract the 'setActiveTabID` method from the DataContext state.
  const [, setActiveTabID] = useContext(DataContext);
  return (
    <div>
      <div onClick={() => setActiveTabID(id)}>{children}</div>
    </div>
  );
}
function TabPanel({ whenActive, children }) {
  //get the 'activeTabID' state from DataContext.
  const [activeTabID] = useContext(DataContext);
  return <div>{activeTabID === whenActive ? children : null}</div>;
}

function TabSwitcher(props) {
  const [activeTabID, setActiveTabID] = useState("a");
  //since this component will provide data to the child components, we will use DataContext.Provider
  return (
    <DataContext.Provider value={[activeTabID, setActiveTabID]}>
      {props.children}
    </DataContext.Provider>
  );
}

export default TabSwitcher;
export { Tab, TabPanel };

The compound component can be used like this:

import TabSwitcher, { Tab, TabPanel } from "./TabSwitcher";

function App() {
  return (
    <div className="App">
      <h1>TabSwitcher with Compound Components</h1>
      <TabSwitcher>
        <Tab id="a">
          <button>a</button>
        </Tab>
        <Tab id="b">
          <button>b</button>
        </Tab>

        <TabPanel whenActive="a">
          <div>a panel</div>
        </TabPanel>

        <TabPanel whenActive="b">
          <div>b panel</div>
        </TabPanel>
      </TabSwitcher>
    </div>
  );
}
export default App;

This will be the output:

final demo of the tabswitcher compound component

And that’s a wrap for your quick guide to React compound components. Thanks for reading!

Full visibility into production React apps

Debugging React applications can be difficult, especially when users experience issues that are hard to reproduce. If you’re interested in monitoring and tracking Redux state, automatically surfacing JavaScript errors, and tracking slow network requests and component load time, try LogRocket.

LogRocket is like a DVR for web apps, recording literally everything that happens on your React app. Instead of guessing why problems happen, you can aggregate and report on what state your application was in when an issue occurred. LogRocket also monitors your app's performance, reporting with metrics like client CPU load, client memory usage, and more.

The LogRocket Redux middleware package adds an extra layer of visibility into your user sessions. LogRocket logs all actions and state from your Redux stores.

Modernize how you debug your React apps — .


Aditya Agarwal Loves experimenting on the web. You can follow me on Twitter @hackerrank.

2 Replies to “Understanding React compound components”

  1. Hello!, this is a nice article, It helped me to understand this pattern a little bit better :), just one observation I have, in the code snippet you have in the article:

    “`
    const Tab = ({ id, children }) => (

    {({ changeTab }) => changeTab(id)}>{children}}

    );
    “`
    there is that ” > ” character in the return of the function inside that is confusing (I even thought it was a special new syntax of react…you never know! lol), then I checked the code sandbox provided, and I saw that the function was actually “({ changeTab }) => changeTab(id)}>{children}” which I was able to understand better.

    Maybe update the article’s code snippets to make it even clearer to new readers with less React experience,

    Thanks!

  2. It seems that the error on the syntax (that weird “>” character) is a problem of this CMS trying to clean up code that is being posted… uhm. well… maybe share the code snippets via https://gist.github.com instead of pasting directly in here. cheers

Leave a Reply