Sandip Nirmal

Compound Components with React Context API

Feb 23, 2019

React is a powerful UI library for developing web and native (React Native) applications. At its core, React utilizes a component-based architecture where every element is constructed as a modular component. For a thorough understanding of this concept, please refer to the official Thinking in React documentation.

In this article, we will explore an advanced concept known as Compound Components and demonstrate their implementation using React (v. 16.3) Context API.

Compound Component

Compound components represent a pattern where multiple components work together cohesively, enabling implicit state sharing between them.

A classic example of this pattern can be found in HTML’s <select> and <option> elements. While these elements have limited functionality when used independently, they create a powerful interaction model when combined through shared state management.

There are two primary approaches to implementing Compound Components in React: utilizing React.cloneElement or implementing the Context API.

The React.cloneElement approach is comprehensively demonstrated by Ryan Florence in the following video:

In this tutorial, we will focus on implementing the same functionality using React Context API.

React Context API

According to the official React documentation, Context is defined as:

Context provides a way to pass data through the component tree without having to pass props down manually at every level.

For those unfamiliar with Context, the following instructional video by Wes Bos provides an excellent introduction:

Final Output will look like this

Compound Tab ComponentCompound Tab Component

Setting up the Context

To implement our compound component, we must first establish the Context using React Context API:

import React from "react";

const TabsContext = React.createContext({
    selectedTabIndex: 0,
    selectTab: () => {},
    headers: [],
    selectedDetails: ''
});

export default TabsContext;

Setting Provider

With the Context established, we implement the Provider component, which serves as the parent container and distributes context values to all child components:

import React from "react";

import TabsContext from "./TabContext";

const tabsData = [
    {name: "EPL",description: "English Premier League"},
    {name: "IPL",description: "Indian Premier League"},
    {name: "Serie A",description: "Italian Football League"}
];

export default class Tabs *extends* React.Component {
    state = {
    selectedTabIndex: 0
    };

    selectTab = selectedTabIndex => {
    this.setState({selectedTabIndex});
    };

    render() {
    const { selectTab, state: { selectedTabIndex }} = this;

    return (
        <TabsContext.Provider value={{
        selectedTabIndex,
        selectTab,
        headers: tabsData.map(({ name }) => ({ name })),
        selectedDetails: tabsData[selectedTabIndex].description
        }}>
        {this.props.children}
    </TabsContext.Provider>
    );
    }
}

Consuming Context Values

With both Context and Provider configured, we can now consume these values within child components, establishing state sharing between Provider and Consumers:

import React from "react";

import TabsContext from "./TabContext";
import Tab from "./Tab";

const TabHeader = () => {

return (
    <div className="tabheader">
    <TabsContext.Consumer>
    {({ headers, selectedTabIndex, selectTab }) => {
        return headers.map(({ name }, index) => (
        <Tab
            name={name}
            key={index}
            selected={selectedTabIndex === index}
            handleClick={() => {
            selectTab(index);
            }}/>
        ));
        }}
        </TabsContext.Consumer>
    </div>);
};

export default TabHeader;

Here, we distribute the headers, selectedTabIndex, and selectTab function from the Provider.

Following the same pattern, we implement the TabDetail component to utilize the selectedDetail value.

Now we can implement our compound component with full functionality:

<Tabs>
    <TabHeader/>
    <TabDetail/>
</Tabs>

This implementation produces the following result:

The component structure is flexible, allowing for various arrangements. For instance, to render the Details component above the Header:

<Tabs>
    <TabDetail/>
    <TabHeader/>
</Tabs>

OutputOutput

Audio Player

To further demonstrate the versatility of Compound Components, I’ve developed an Audio Player implementation, similar to the concept presented in the second video referenced above. The final implementation appears as follows:

Audio Player Final ExampleAudio Player Final Example

The Audio Player’s state is shared among all child components, enabling various implementation configurations:

Full Control

Audio Player with Full ControlsAudio Player with Full Controls

Play Control

Audio Player with Only Play Control and ProgressAudio Player with Only Play Control and Progress

Play and Pause Control

Audio Player with Only Play and Pause ControlAudio Player with Only Play and Pause Control

Play/Pause Control

Audio Player with Play/Pause ControlAudio Player with Play/Pause Control

The complete source code for both examples is available here.

Conclusion

Compound components effectively eliminate the need for conditional and repetitive code. For instance, in our example, rearranging the order of TabDetails and TabHeader components doesn’t require additional conditional logic within the Tabs component.

Important Note: Exercise caution when using Context API for compound components within loops (map operations). List items, in particular, should not be implemented as compound components using Context API, as this creates individual providers at each item level, potentially impacting performance.