DEV Community

Cover image for Advanced Patterns for Compound Components in React: Building Flexible UIs with Context and cloneElement
Ishan Bagchi for Byte-Sized News

Posted on • Originally published at Medium

Advanced Patterns for Compound Components in React: Building Flexible UIs with Context and cloneElement

When building complex user interfaces in React, one of the most powerful patterns you can use is compound components. This pattern allows you to create components that work together seamlessly, providing a flexible and intuitive API for developers. In this blog, we’ll explore how to design compound components using React’s Context API and cloneElement, and we’ll walk through examples like building a flexible Tabs or Accordion component.


What Are Compound Components?

Compound components are a pattern where a parent component works in tandem with one or more child components to create a cohesive UI. Think of components like <select> and <option> in HTML—they work together to create a dropdown menu. In React, we can create similar relationships between components to build reusable and customizable UIs.

The key benefits of compound components are:

  • Flexibility: They allow developers to compose components in a way that makes sense for their use case.
  • Encapsulation: The logic is centralized in the parent component, while the child components handle rendering.
  • Readability: The resulting code is declarative and easy to understand.

Designing Compound Components with Context and cloneElement

To implement compound components, we can use two powerful tools in React:

  1. React Context: For sharing state and logic between the parent and child components.
  2. React.cloneElement: For dynamically passing props to child components.

Let’s break down how these tools work together.


Example 1: Building a Flexible Tabs Component

A Tabs component is a great example of compound components. It typically consists of a Tabs parent, TabList for the tab headers, and TabPanel for the content. Here’s how we can build it:

Step 1: Create the Context

First, we’ll create a context to share the active tab state and a function to switch tabs.

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

const TabsContext = createContext();

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

  return (
    <TabsContext.Provider value={{ activeIndex, setActiveIndex }}>
      <div className="tabs">{children}</div>
    </TabsContext.Provider>
  );
};
Enter fullscreen mode Exit fullscreen mode

Step 2: Create the TabList and Tab Components

Next, we’ll create the TabList and Tab components. These will use the context to determine which tab is active and handle click events.

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

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

  return (
    <button
      className={`tab ${index === activeIndex ? 'active' : ''}`}
      onClick={() => setActiveIndex(index)}
    >
      {children}
    </button>
  );
};
Enter fullscreen mode Exit fullscreen mode

Step 3: Create the TabPanel Component

Finally, we’ll create the TabPanel component, which will only render its content if its index matches the active tab.

const TabPanel = ({ index, children }) => {
  const { activeIndex } = useContext(TabsContext);

  return index === activeIndex ? <div className="tab-panel">{children}</div> : null;
};
Enter fullscreen mode Exit fullscreen mode

Step 4: Putting It All Together

Now, we can use our Tabs component like this:

<Tabs defaultIndex={0}>
  <TabList>
    <Tab index={0}>Tab 1</Tab>
    <Tab index={1}>Tab 2</Tab>
    <Tab index={2}>Tab 3</Tab>
  </TabList>
  <TabPanel index={0}>Content for Tab 1</TabPanel>
  <TabPanel index={1}>Content for Tab 2</TabPanel>
  <TabPanel index={2}>Content for Tab 3</TabPanel>
</Tabs>
Enter fullscreen mode Exit fullscreen mode

Example 2: Building an Accordion Component

Let’s take this pattern a step further and build an Accordion component. An accordion typically consists of a parent Accordion component and child AccordionItem components.

Step 1: Create the Context

We’ll use context to manage which accordion item is currently open.

const AccordionContext = createContext();

const Accordion = ({ children }) => {
  const [openIndex, setOpenIndex] = useState(null);

  return (
    <AccordionContext.Provider value={{ openIndex, setOpenIndex }}>
      <div className="accordion">{children}</div>
    </AccordionContext.Provider>
  );
};
Enter fullscreen mode Exit fullscreen mode

Step 2: Create the AccordionItem Component

The AccordionItem will use cloneElement to pass additional props to its children.

const AccordionItem = ({ index, children }) => {
  const { openIndex, setOpenIndex } = useContext(AccordionContext);

  return React.Children.map(children, (child) =>
    React.cloneElement(child, {
      isOpen: index === openIndex,
      onClick: () => setOpenIndex(index === openIndex ? null : index),
    })
  );
};
Enter fullscreen mode Exit fullscreen mode

Step 3: Create the AccordionHeader and AccordionContent Components

These components will receive props from AccordionItem to handle their behavior.

const AccordionHeader = ({ children, isOpen, onClick }) => (
  <button className={`accordion-header ${isOpen ? 'open' : ''}`} onClick={onClick}>
    {children}
  </button>
);

const AccordionContent = ({ children, isOpen }) => (
  <div className={`accordion-content ${isOpen ? 'open' : ''}`}>
    {isOpen && children}
  </div>
);
Enter fullscreen mode Exit fullscreen mode

Step 4: Using the Accordion Component

Here’s how you can use the Accordion component:

<Accordion>
  <AccordionItem index={0}>
    <AccordionHeader>Header 1</AccordionHeader>
    <AccordionContent>Content for Item 1</AccordionContent>
  </AccordionItem>
  <AccordionItem index={1}>
    <AccordionHeader>Header 2</AccordionHeader>
    <AccordionContent>Content for Item 2</AccordionContent>
  </AccordionItem>
</Accordion>
Enter fullscreen mode Exit fullscreen mode

Why Use Compound Components?

Compound components are a fantastic way to build reusable and flexible UIs. By leveraging React Context and cloneElement, you can create components that are both powerful and easy to use. Whether you’re building a Tabs component, an Accordion, or something entirely custom, this pattern will help you write cleaner, more maintainable code.

So, the next time you’re designing a complex UI, consider using compound components—it might just be the perfect solution!

Top comments (0)