Compound Components

Hard

Compound components are related components that share implicit state through Context, forming a cohesive API. The parent manages state and children consume it, giving users full control over rendering order and composition while hiding internal wiring. Libraries like Radix UI use this pattern extensively.

Interactive Visualization

Key Points

  • Parent component manages shared state via Context Provider
  • Child components consume the shared context to coordinate behavior
  • Consumers control rendering order and composition freely
  • Internal state sharing is hidden from the public API
  • Validation can enforce that children are used within the correct parent
  • Named sub-components (e.g., Tabs.Panel) provide a clear API

Code Examples

Tabs Compound Component

import { createContext, useContext, useState, type ReactNode } from 'react'

interface TabsContextValue {
  activeTab: string
  setActiveTab: (id: string) => void
}

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

function useTabsContext(): TabsContextValue {
  const ctx = useContext(TabsContext)
  if (!ctx) throw new Error('Tab components must be used within Tabs')
  return ctx
}

function Tabs({ children, defaultTab }: { children: ReactNode; defaultTab: string }) {
  const [activeTab, setActiveTab] = useState(defaultTab)
  return (
    <TabsContext.Provider value={{ activeTab, setActiveTab }}>
      {children}
    </TabsContext.Provider>
  )
}

function TabList({ children }: { children: ReactNode }) {
  return <div role="tablist">{children}</div>
}

function Tab({ id, children }: { id: string; children: ReactNode }) {
  const { activeTab, setActiveTab } = useTabsContext()
  return (
    <button
      role="tab"
      aria-selected={activeTab === id}
      onClick={() => setActiveTab(id)}
    >
      {children}
    </button>
  )
}

function TabPanel({ id, children }: { id: string; children: ReactNode }) {
  const { activeTab } = useTabsContext()
  return activeTab === id ? <div role="tabpanel">{children}</div> : null
}

// Usage
function App() {
  return (
    <Tabs defaultTab="overview">
      <TabList>
        <Tab id="overview">Overview</Tab>
        <Tab id="details">Details</Tab>
      </TabList>
      <TabPanel id="overview">Overview content</TabPanel>
      <TabPanel id="details">Details content</TabPanel>
    </Tabs>
  )
}

Each sub-component consumes the shared TabsContext, allowing consumers to freely compose and reorder Tab and TabPanel elements

Common Mistakes

  • Using compound components for simple cases where props would be cleaner
  • Not providing a helpful error message when child components are used outside the parent
  • Exposing too much internal state through the shared context

Interview Tips

  • Compare compound components to configuration-based APIs and explain the flexibility trade-off
  • Reference real-world libraries like Radix UI or Headless UI that use this pattern
  • Discuss how context keeps the internal wiring hidden from consumers

Related Concepts