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:
- React Context: For sharing state and logic between the parent and child components.
-
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>
);
};
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>
);
};
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;
};
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>
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>
);
};
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),
})
);
};
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>
);
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>
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)