Compound Components in React.js

In React.js, Compound Components is an advanced design pattern that allows you to build components that are more flexible and customizable. This pattern is particularly useful when you want to give users the freedom to compose and structure components in a variety of ways while keeping the code organized and reusable.

What Are Compound Components?

Compound Components are a way to create React components that work together as a cohesive unit. They allow you to design a group of related components that communicate with each other through a shared context. This pattern is especially useful for building components like tabs, modals, dropdown menus, or forms where the parent component controls the behavior and the children manage specific tasks.

In simpler terms, a Compound Component pattern allows you to break down a component into smaller parts, which are self-contained but work together when combined.

Why Use Compound Components?

  • Flexibility: They allow users of your components to have more control over how they structure and arrange the components.
  • Clean API: The interface for using compound components is usually clean and easy to understand.
  • Reusability: Compound Components encourage code reuse, reducing duplication and improving maintainability.

Basic Example of Compound Components

Let’s start with a simple example of a Compound Component that implements a toggle button.

Basic Toggle Button Example

We want to build a toggle button with two components: Toggle (the parent) and ToggleOn, ToggleOff (the children). The parent will control the state, and the children will display content based on the state.

				
					import React, { useState } from 'react';

const Toggle = ({ children }) => {
    const [on, setOn] = useState(false);
    
    const toggle = () => setOn(!on);
    
    return React.Children.map(children, child => {
        if (typeof child.type === 'function') {
            return React.cloneElement(child, { on, toggle });
        }
        return child;
    });
};

const ToggleOn = ({ on, children }) => on ? <>{children}</> : null;
const ToggleOff = ({ on, children }) => !on ? <>{children}</> : null;
const ToggleButton = ({ toggle }) => <button onClick={toggle}>Toggle</button>;

const App = () => (
    <Toggle>
        <ToggleOn>The button is ON</ToggleOn>
        <ToggleOff>The button is OFF</ToggleOff>
        <ToggleButton />
    </Toggle>
);

				
			

Explanation

  • Toggle Component: This is the parent component that manages the toggle state (on or off). It uses the React.Children.map method to pass down the state (on) and the toggle function to its children.
  • ToggleOn and ToggleOff: These are child components that decide what to render based on the on state. If on is true, ToggleOn shows its content; if false, ToggleOff shows its content.
  • ToggleButton: A button component that calls the toggle function to change the state.

Output

  • When the button is clicked, the text alternates between “The button is ON” and “The button is OFF”.

Deep Dive into Compound Components

Now that we understand the basic structure, let’s dive deeper into the internal workings of Compound Components and explore more advanced techniques.

Sharing State Between Components

One of the core ideas behind Compound Components is state sharing. In our previous example, the Toggle component held the state (on) and passed it down to ToggleOn, ToggleOff, and ToggleButton. This allowed these child components to act based on the parent’s state.

This state-sharing is usually done using React Context when the structure becomes more complex.

Using Context in Compound Components

For large components, passing props manually can become cumbersome. Instead, we can use React Context to share state between parent and child components without passing props directly. This makes the code more scalable.

Let’s update our toggle example using Context API.

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

// Create a Context
const ToggleContext = createContext();

// Toggle Provider to share state with children
const Toggle = ({ children }) => {
    const [on, setOn] = useState(false);
    const toggle = () => setOn(!on);

    return (
        <ToggleContext.Provider value={{ on, toggle }}>
            {children}
        </ToggleContext.Provider>
    );
};

// Custom hook to access Toggle Context
const useToggle = () => {
    const context = useContext(ToggleContext);
    if (!context) {
        throw new Error("useToggle must be used within a Toggle");
    }
    return context;
};

const ToggleOn = ({ children }) => {
    const { on } = useToggle();
    return on ? <>{children}</> : null;
};

const ToggleOff = ({ children }) => {
    const { on } = useToggle();
    return !on ? <>{children}</> : null;
};

const ToggleButton = () => {
    const { toggle } = useToggle();
    return <button onClick={toggle}>Toggle</button>;
};

const App = () => (
    <Toggle>
        <ToggleOn>The button is ON</ToggleOn>
        <ToggleOff>The button is OFF</ToggleOff>
        <ToggleButton />
    </Toggle>
);

				
			

Explanation

  • ToggleContext: This context provides the on state and toggle function to all child components without having to pass props.
  • useToggle: A custom hook that allows any component inside the Toggle component to access the context values.
  • Toggle Provider: It wraps the children with ToggleContext.Provider to share the state (on) and the toggle function.

Output

This behaves the same as the previous example, but now the state and functions are shared more cleanly using Context.

Advanced Topics in Compound Components

Dynamic Children

One powerful feature of Compound Components is the ability to handle dynamic children. Instead of hardcoding child components like ToggleOn and ToggleOff, you can make your compound components more flexible by allowing dynamic content.

				
					const App = () => (
    <Toggle>
        <ToggleOn>The button is ON</ToggleOn>
        <ToggleOff>The button is OFF</ToggleOff>
        <ToggleButton />
        <div>Some other content</div>
    </Toggle>
);

				
			

In this example, Toggle will work with any content that is passed to it, not just ToggleOn or ToggleOff.

Customizing Behavior with Props

You can also allow users to customize the behavior of compound components using props. For example, you might want to allow users to control how the Toggle component behaves when clicked.

				
					const Toggle = ({ children, initialOn = false }) => {
    const [on, setOn] = useState(initialOn);
    const toggle = () => setOn(!on);

    return React.Children.map(children, child => {
        return React.cloneElement(child, { on, toggle });
    });
};

				
			

Now, users of the Toggle component can pass an initialOn prop to control the initial state.

				
					<Toggle initialOn={true}>
    <ToggleOn>The button is ON</ToggleOn>
    <ToggleOff>The button is OFF</ToggleOff>
    <ToggleButton />
</Toggle>

				
			

Real-World Example: Building a Tab Component

Let’s build a more complex example to see how Compound Components can be used in a real-world scenario. We’ll create a Tab Component with multiple tabs that users can click to view different content.

Tab Component Example

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

// Create Context
const TabContext = createContext();

const Tabs = ({ children }) => {
    const [activeIndex, setActiveIndex] = useState(0);

    return (
        <TabContext.Provider value={{ activeIndex, setActiveIndex }}>
            {children}
        </TabContext.Provider>
    );
};

const TabList = ({ children }) => {
    return <div className="tab-list">{children}</div>;
};

const Tab = ({ index, children }) => {
    const { activeIndex, setActiveIndex } = useContext(TabContext);
    const isActive = activeIndex === index;

    return (
        <button
            className={`tab ${isActive ? 'active' : ''}`}
            onClick={() => setActiveIndex(index)}
        >
            {children}
        </button>
    );
};

const TabPanels = ({ children }) => {
    const { activeIndex } = useContext(TabContext);
    return <div>{children[activeIndex]}</div>;
};

const TabPanel = ({ children }) => {
    return <div>{children}</div>;
};

// Usage
const App = () => (
    <Tabs>
        <TabList>
            <Tab index={0}>Tab 1</Tab>
            <Tab index={1}>Tab 2</Tab>
            <Tab index={2}>Tab 3</Tab>
        </TabList>
        <TabPanels>
            <TabPanel>Content 1</TabPanel>
            <TabPanel>Content 2</TabPanel>
            <TabPanel>Content 3</TabPanel>
        </TabPanels>
    </Tabs>
);

				
			

Explanation

  • Tabs: This is the parent component that manages the active tab index.
  • TabList: A wrapper for the list of tabs.
  • Tab: Each individual tab button. When clicked, it sets the active tab.
  • TabPanels: The container for the content of each tab. It renders the content of the active tab.
  • TabPanel: Individual panel content corresponding to each tab.

Output

  • The application will render a tabbed interface where clicking each tab shows the corresponding content in the panel below.

Compound Components are a powerful pattern in React.js that allow you to create flexible, reusable, and scalable UI components. By leveraging concepts like state sharing and the Context API, you can make components that work together seamlessly while offering flexibility to the developer using them. Happy Coding!❤️

Table of Contents