Compound Components in React

Loading

Compound components are a pattern in React where multiple components work together to form a cohesive UI while maintaining a shared implicit state. This pattern is particularly useful for building reusable, flexible components like tabs, accordions, or forms.

How Compound Components Work

The key idea is that parent and child components share state and behavior implicitly, without the need for explicit prop drilling. This creates a more intuitive API for consumers of your component.

Basic Example

import { useState } from 'react';

function Tabs({ children }) {
  const [activeTab, setActiveTab] = useState(0);

  return (
    <div className="tabs">
      {children.map((child, index) => (
        React.cloneElement(child, {
          isActive: index === activeTab,
          onSelect: () => setActiveTab(index),
          key: index
        })
      ))}
    </div>
  );
}

function Tab({ isActive, onSelect, children }) {
  return (
    <button
      onClick={onSelect}
      style={{ fontWeight: isActive ? 'bold' : 'normal' }}
    >
      {children}
    </button>
  );
}

// Usage
function App() {
  return (
    <Tabs>
      <Tab>First Tab</Tab>
      <Tab>Second Tab</Tab>
      <Tab>Third Tab</Tab>
    </Tabs>
  );
}

Advantages of Compound Components

  1. Better Separation of Concerns: Each component handles its own part of the UI
  2. More Flexible API: Consumers can rearrange or customize parts of the component
  3. Implicit State Sharing: State is managed internally without requiring consumers to handle it
  4. Cleaner JSX: Results in more readable component composition

Implementing with Context API

For more complex cases, React’s Context API is often used with compound components:

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

const TabsContext = createContext();

function Tabs({ children }) {
  const [activeTab, setActiveTab] = useState(0);

  return (
    <TabsContext.Provider value={{ activeTab, setActiveTab }}>
      <div className="tabs">{children}</div>
    </TabsContext.Provider>
  );
}

function Tab({ index, children }) {
  const { activeTab, setActiveTab } = useContext(TabsContext);

  return (
    <button
      onClick={() => setActiveTab(index)}
      style={{ fontWeight: index === activeTab ? 'bold' : 'normal' }}
    >
      {children}
    </button>
  );
}

function TabPanel({ index, children }) {
  const { activeTab } = useContext(TabsContext);
  return activeTab === index ? <div>{children}</div> : null;
}

// Usage
function App() {
  return (
    <Tabs>
      <div className="tab-list">
        <Tab index={0}>First</Tab>
        <Tab index={1}>Second</Tab>
      </div>
      <TabPanel index={0}>First content</TabPanel>
      <TabPanel index={1}>Second content</TabPanel>
    </Tabs>
  );
}

Real-world Examples

Popular libraries use compound components:

  • React Router’s <BrowserRouter>, <Route>, <Link>
  • Reach UI’s <Menu>, <MenuButton>, <MenuList>, <MenuItem>
  • Downshift’s autocomplete components

Best Practices

  1. Use React.Children.map and React.cloneElement for simple cases
  2. Use Context API for more complex state sharing
  3. Provide sensible defaults where possible
  4. Document the expected structure of child components
  5. Consider TypeScript for better type safety with compound components

Compound components are a powerful pattern that can make your React components more flexible and maintainable when you need to create complex, reusable UI elements.

Leave a Reply

Your email address will not be published. Required fields are marked *