Mastering Compound Components in React: The Art of Component Composition

Mastering Compound Components in React: The Art of Component Composition
Photo by Khara Woods / Unsplash

If you’ve spent some time working with React, chances are you’ve come across a syntax like Form.Input or Menu.Item. It may look a little unusual at first, but it represents a powerful pattern known as the Compound Component. This design pattern enables developers to build highly flexible and expressive component APIs, particularly useful for building design systems or UI libraries.

In this article, we’ll explore what compound components are, why they matter, how to implement them, and a few gotchas to keep in mind along the way.


What Are Compound Components?

In essence, compound components are a set of components that work together as a unified abstraction, often sharing internal logic or state. Instead of scattering related components everywhere and hoping users will wire them up correctly, you encapsulate the relationship within a parent component.

Think of it like this:

<Form>
  <Form.Label>Email</Form.Label>
  <Form.Input type="email" />
</Form>

Here, Form.Label and Form.Input aren’t separate components imported from different files. They are attached to the Form component itself. This makes it clear that these pieces belong together — they are part of the same conceptual “form unit.”


Why Use Compound Components?

Better API Design

Attaching subcomponents to a parent creates a cohesive namespace that makes the API more readable and intuitive.

// Better than this:
import { FormLabel, FormInput } from "./components";

<FormLabel />
<FormInput />

By using compound components:

<Form>
  <Form.Label />
  <Form.Input />
</Form>

You clearly communicate hierarchy and responsibility.

Logical Grouping

It’s easier for other developers (or future-you) to discover available subcomponents without needing to search through docs or code. Just inspecting Form. gives you all related elements.

Shared State & Logic

Compound components often leverage React context internally to share state between the parent and children automatically. This removes the need to manually pass props like value, onChange, etc.


How to Build a Compound Component in React

Let’s walk through a simple example using a Tabs component.

// Tabs.js
import { createContext, useContext, useState } from 'react';

const TabsContext = createContext();

function Tabs({ children }) {
  const [activeIndex, setActiveIndex] = useState(0);
  return (
    <TabsContext.Provider value={{ activeIndex, setActiveIndex }}>
      <div className="tabs">{children}</div>
    </TabsContext.Provider>
  );
}

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

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

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

function TabPanel({ index, children }) {
  const { activeIndex } = useContext(TabsContext);
  return activeIndex === index ? <div className="tab-panel">{children}</div> : null;
}

// Attach children
Tabs.List = TabList;
Tabs.Tab = Tab;
Tabs.Panel = TabPanel;

export default Tabs;

Usage:

<Tabs>
  <Tabs.List>
    <Tabs.Tab index={0}>Home</Tabs.Tab>
    <Tabs.Tab index={1}>Profile</Tabs.Tab>
  </Tabs.List>
  <Tabs.Panel index={0}>Welcome Home!</Tabs.Panel>
  <Tabs.Panel index={1}>Your Profile</Tabs.Panel>
</Tabs>

Gotchas & Considerations

Be Careful with Props Drilling

If you don’t use React context, passing props manually between parent and subcomponents can become messy. Compound components shine when you combine them with context API to share logic cleanly.

Testing Can Be Tricky

When you nest components like Form.Input, your tests may require extra steps to import or reference subcomponents properly. A good workaround is to export subcomponents separately as well, in case they're needed independently.

Intellisense May Struggle

Depending on your editor or tooling, auto-completion for static properties (like Form.Input) may not always work out of the box. TypeScript users can manually declare static members on the parent component to fix this.


When to Use Compound Components

Use this pattern when:

  • You’re building design systems or UI libraries
  • You need tight coordination between components
  • You want to provide a clear and discoverable API
  • You’re working on composable abstractions like Tabs, Forms, Modals, etc.

Avoid it if:

  • The components are completely independent
  • You want to allow maximum flexibility or usage outside a specific structure

Finally

The Compound Component pattern is one of React's most elegant patterns for building modular and expressive UI components. When used well, it enhances maintainability, readability, and reusability — especially in large codebases or shared component libraries.

If you’re building something like a Dropdown, Accordion, Tabs, or Form, consider implementing it using this approach. Your future self — and your teammates — will thank you.

Support Us