Skip to main content
Technology & EngineeringReact Patterns168 lines

Compound Components

Compound component pattern for building flexible, implicitly-shared React component APIs

Quick Summary28 lines
You are an expert in the Compound Components pattern for building React applications.

## Key Points

- **Implicit State Sharing**: Parent owns the state; children read it through context without explicit props.
- **Inversion of Control**: The consumer decides which sub-components to render and in what order.
- **Declarative API**: Usage reads like a domain-specific language — `<Select>`, `<Select.Option>`, `<Select.Trigger>`.
- **Static Sub-component Attachment**: Sub-components are typically attached as static properties on the parent (`Parent.Child`).
- Always validate context usage with a custom hook that throws if the provider is missing.
- Attach sub-components as static properties (`Parent.Child = Child`) for discoverable APIs.
- Keep the parent component focused on state management, not rendering.
- Use TypeScript generics when the compound component wraps varying data types.
- Document which sub-components are required vs. optional.
- **Forgetting the context guard**: Consumers get cryptic errors when a child is rendered outside the parent.
- **Over-coupling sub-components**: Each sub-component should depend only on the shared context, not on sibling components.
- **Breaking composition with `React.Children.map`**: Older implementations that clone children break when wrappers are introduced; prefer context over `cloneElement`.

## Quick Example

```tsx
<Toggle>
  <Toggle.On>The toggle is on</Toggle.On>
  <Toggle.Off>The toggle is off</Toggle.Off>
  <Toggle.Button />
</Toggle>
```
skilldb get react-patterns-skills/Compound ComponentsFull skill: 168 lines
Paste into your CLAUDE.md or agent config

Compound Components — React Patterns

You are an expert in the Compound Components pattern for building React applications.

Overview

Compound components are a set of components that work together to form a complete UI element while sharing implicit state. The parent component manages the state, and child components consume it via React context, giving consumers full control over rendering order and composition without prop drilling.

Core Philosophy

Compound components exist because the alternative — a single monolithic component with dozens of props — becomes impossible to maintain and hostile to consumers. When you design a <Select> that accepts options, renderOption, onSelect, placeholder, isMulti, isSearchable, and twenty more props, you are making every decision for the consumer upfront and forcing them to communicate through an increasingly bloated configuration object. Compound components flip this relationship: the parent owns the state, and the consumer composes the pieces they need in the order they want.

This pattern is fundamentally about respecting the consumer's autonomy. Instead of encoding every possible layout and behavior permutation into props, you provide building blocks that share implicit state through context. The consumer decides which blocks to use, how to arrange them, and what to wrap them with. This is React's composition model taken to its logical conclusion — components that are designed to be assembled, not configured.

The discipline required is restraint. Each sub-component should do one thing and depend only on the shared context, never on the presence or position of its siblings. When you feel the urge to add a prop to the parent that controls how a child renders, that is a signal you should create a new sub-component instead. The API surface grows by adding composable pieces, not by inflating a props interface.

Core Concepts

  • Implicit State Sharing: Parent owns the state; children read it through context without explicit props.
  • Inversion of Control: The consumer decides which sub-components to render and in what order.
  • Declarative API: Usage reads like a domain-specific language — <Select>, <Select.Option>, <Select.Trigger>.
  • Static Sub-component Attachment: Sub-components are typically attached as static properties on the parent (Parent.Child).

Implementation Patterns

Basic Compound Component with Context

import { createContext, useContext, useState, ReactNode } from "react";

interface ToggleContextValue {
  on: boolean;
  toggle: () => void;
}

const ToggleContext = createContext<ToggleContextValue | null>(null);

function useToggleContext() {
  const ctx = useContext(ToggleContext);
  if (!ctx) throw new Error("Toggle compound components must be used within <Toggle>");
  return ctx;
}

function Toggle({ children }: { children: ReactNode }) {
  const [on, setOn] = useState(false);
  const toggle = () => setOn((prev) => !prev);
  return (
    <ToggleContext.Provider value={{ on, toggle }}>
      {children}
    </ToggleContext.Provider>
  );
}

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

function ToggleOff({ children }: { children: ReactNode }) {
  const { on } = useToggleContext();
  return on ? null : <>{children}</>;
}

function ToggleButton() {
  const { on, toggle } = useToggleContext();
  return <button onClick={toggle}>{on ? "ON" : "OFF"}</button>;
}

Toggle.On = ToggleOn;
Toggle.Off = ToggleOff;
Toggle.Button = ToggleButton;

export { Toggle };

Usage

<Toggle>
  <Toggle.On>The toggle is on</Toggle.On>
  <Toggle.Off>The toggle is off</Toggle.Off>
  <Toggle.Button />
</Toggle>

Advanced: Compound Tabs Component

import { createContext, useContext, useState, ReactNode } from "react";

interface TabsContextValue {
  activeIndex: number;
  setActiveIndex: (index: number) => void;
}

const TabsContext = createContext<TabsContextValue | null>(null);

function useTabs() {
  const ctx = useContext(TabsContext);
  if (!ctx) throw new Error("Tabs components must be rendered inside <Tabs>");
  return ctx;
}

function Tabs({ children, defaultIndex = 0 }: { children: ReactNode; defaultIndex?: number }) {
  const [activeIndex, setActiveIndex] = useState(defaultIndex);
  return (
    <TabsContext.Provider value={{ activeIndex, setActiveIndex }}>
      <div role="tablist">{children}</div>
    </TabsContext.Provider>
  );
}

function Tab({ index, children }: { index: number; children: ReactNode }) {
  const { activeIndex, setActiveIndex } = useTabs();
  return (
    <button
      role="tab"
      aria-selected={activeIndex === index}
      onClick={() => setActiveIndex(index)}
    >
      {children}
    </button>
  );
}

function TabPanel({ index, children }: { index: number; children: ReactNode }) {
  const { activeIndex } = useTabs();
  return activeIndex === index ? <div role="tabpanel">{children}</div> : null;
}

Tabs.Tab = Tab;
Tabs.Panel = TabPanel;

export { Tabs };

Best Practices

  • Always validate context usage with a custom hook that throws if the provider is missing.
  • Attach sub-components as static properties (Parent.Child = Child) for discoverable APIs.
  • Keep the parent component focused on state management, not rendering.
  • Use TypeScript generics when the compound component wraps varying data types.
  • Document which sub-components are required vs. optional.

Common Pitfalls

  • Forgetting the context guard: Consumers get cryptic errors when a child is rendered outside the parent.
  • Over-coupling sub-components: Each sub-component should depend only on the shared context, not on sibling components.
  • Breaking composition with React.Children.map: Older implementations that clone children break when wrappers are introduced; prefer context over cloneElement.
  • Leaking internal state: Expose only the minimal context value needed by sub-components; keep implementation details private.

Anti-Patterns

  • The God Component with a render map: Instead of compound components, creating a single component that accepts an array of configuration objects and internally maps them to sub-renders. This removes consumer control over composition, makes TypeScript types unwieldy, and breaks when consumers need to insert custom elements between items.

  • Sibling-dependent sub-components: Building sub-components that query or depend on the existence of other sub-components (e.g., Tab that breaks without TabPanel being a direct sibling). Sub-components should depend only on the shared context, never on each other's presence or ordering in the tree.

  • Cloning children instead of using context: Using React.Children.map and cloneElement to inject props into direct children. This breaks the moment a consumer wraps a child in a <div> or a fragment, creating fragile APIs that fail silently.

  • Exposing internal dispatch in context: Putting raw setState or dispatch functions into the context value instead of purpose-built callbacks. This leaks implementation details and makes it easy for consumers to put the component into invalid states.

  • Recreating existing HTML semantics: Building compound components for things that native HTML already handles well (e.g., a compound <Form> with <Form.Input> and <Form.Label> that add no behavior beyond what <form>, <input>, and <label> provide). Compound components should earn their complexity by managing state that HTML cannot.

Install this skill directly: skilldb add react-patterns-skills

Get CLI access →