Mastering the Compound Pattern in React: Building Declarative and Flexible Components with TypeScript

The Compound Pattern in React represents a powerful design approach that enables developers to create flexible, reusable components that work together seamlessly. This pattern addresses common challenges like prop drilling while promoting declarative code that's both readable and maintainable. This comprehensive guide explores the compound pattern through practical TypeScript examples, performance considerations, and advanced implementations.

Understanding the Compound Pattern

The Compound Pattern involves creating a parent component that manages state and behavior, while child components consume and display this state through a shared context. Compound components are a pattern in React, where several components are used together such that they share an implicit state that allows them to communicate with each other in the background. This pattern is particularly effective when multiple components need to work together to accomplish a shared task.

What Makes It Special?

Compound components can be said to be a pattern that encloses the state and the behavior of a group of components but still gives the rendering control of its variable parts back to the external user. This approach provides several key advantages:

  • Declarative API: Components express what they do, not how they do it
  • Flexible Composition: Users can arrange child components in any order
  • Implicit State Sharing: Components communicate without explicit prop passing
  • Enhanced Reusability: Each component can be used independently when needed
  • Type Safety: TypeScript provides excellent support for this pattern

The Prop Drilling Problem

Before diving into the compound pattern, let's examine the traditional prop drilling approach and its limitations.

Traditional Prop Drilling Example

Consider a simple menu component built with traditional prop drilling:

// Traditional approach with prop drilling
interface MenuItem {
  id: string;
  label: string;
}

interface MenuProps {
  isOpen: boolean;
  onToggle: () => void;
  items: MenuItem[];
  activeItem: string | null;
  onItemClick: (id: string) => void;
}

function Menu({ isOpen, onToggle, items, activeItem, onItemClick }: MenuProps) {
  return (
    <div className="menu">
      <MenuButton isOpen={isOpen} onToggle={onToggle} />
      <MenuList 
        isOpen={isOpen} 
        items={items} 
        activeItem={activeItem} 
        onItemClick={onItemClick} 
      />
    </div>
  );
}

interface MenuButtonProps {
  isOpen: boolean;
  onToggle: () => void;
}

function MenuButton({ isOpen, onToggle }: MenuButtonProps) {
  return (
    <button onClick={onToggle}>
      {isOpen ? 'Close' : 'Open'} Menu
    </button>
  );
}

interface MenuListProps {
  isOpen: boolean;
  items: MenuItem[];
  activeItem: string | null;
  onItemClick: (id: string) => void;
}

function MenuList({ isOpen, items, activeItem, onItemClick }: MenuListProps) {
  if (!isOpen) return null;
  
  return (
    <ul>
      {items.map(item => (
        <MenuItemComponent
          key={item.id}
          item={item}
          isActive={item.id === activeItem}
          onItemClick={onItemClick}
        />
      ))}
    </ul>
  );
}

interface MenuItemComponentProps {
  item: MenuItem;
  isActive: boolean;
  onItemClick: (id: string) => void;
}

function MenuItemComponent({ item, isActive, onItemClick }: MenuItemComponentProps) {
  return (
    <li 
      className={isActive ? 'active' : ''}
      onClick={() => onItemClick(item.id)}
    >
      {item.label}
    </li>
  );
}

// Usage
function App() {
  const [isOpen, setIsOpen] = useState(false);
  const [activeItem, setActiveItem] = useState<string | null>(null);
  const items: MenuItem[] = [
    { id: '1', label: 'Home' },
    { id: '2', label: 'About' },
    { id: '3', label: 'Contact' }
  ];

  return (
    <Menu
      isOpen={isOpen}
      onToggle={() => setIsOpen(!isOpen)}
      items={items}
      activeItem={activeItem}
      onItemClick={setActiveItem}
    />
  );
}

Problems with This Approach

The traditional approach presents several challenges:

  • Tight Coupling: Components are tightly bound to their parent's prop structure
  • Verbose Props: Every intermediate component must accept and pass props
  • Limited Flexibility: Changing the component structure requires updating multiple prop interfaces
  • Poor Reusability: Components become difficult to reuse in different contexts
  • Complex Type Definitions: Managing types across multiple levels becomes cumbersome

Compound Pattern Solution

The compound pattern elegantly solves these problems by using React Context to share state implicitly among related components.

Basic Compound Pattern Implementation

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

// Define the context type
interface MenuContextType {
  isOpen: boolean;
  setIsOpen: (isOpen: boolean) => void;
  activeItem: string | null;
  setActiveItem: (id: string | null) => void;
  toggle: () => void;
  selectItem: (id: string) => void;
}

// Create context for the compound component
const MenuContext = createContext<MenuContextType | undefined>(undefined);

// Custom hook to access menu context
function useMenuContext(): MenuContextType {
  const context = useContext(MenuContext);
  if (!context) {
    throw new Error('Menu compound components must be used within Menu');
  }
  return context;
}

// Main Menu component interface
interface MenuProps {
  children: ReactNode;
}

// Main Menu component that provides context
function Menu({ children }: MenuProps) {
  const [isOpen, setIsOpen] = useState(false);
  const [activeItem, setActiveItem] = useState<string | null>(null);

  const contextValue: MenuContextType = {
    isOpen,
    setIsOpen,
    activeItem,
    setActiveItem,
    toggle: () => setIsOpen(!isOpen),
    selectItem: (id: string) => setActiveItem(id)
  };

  return (
    <MenuContext.Provider value={contextValue}>
      <div className="menu">
        {children}
      </div>
    </MenuContext.Provider>
  );
}

// Compound components with proper typing
interface MenuButtonProps {
  children?: ReactNode;
}

Menu.Button = function MenuButton({ children }: MenuButtonProps) {
  const { isOpen, toggle } = useMenuContext();
  
  return (
    <button onClick={toggle}>
      {children || (isOpen ? 'Close Menu' : 'Open Menu')}
    </button>
  );
};

interface MenuListProps {
  children: ReactNode;
}

Menu.List = function MenuList({ children }: MenuListProps) {
  const { isOpen } = useMenuContext();
  
  if (!isOpen) return null;
  
  return <ul className="menu-list">{children}</ul>;
};

interface MenuItemProps {
  id: string;
  children: ReactNode;
}

Menu.Item = function MenuItem({ id, children }: MenuItemProps) {
  const { activeItem, selectItem } = useMenuContext();
  const isActive = activeItem === id;
  
  return (
    <li 
      className={`menu-item ${isActive ? 'active' : ''}`}
      onClick={() => selectItem(id)}
    >
      {children}
    </li>
  );
};

// Clean, declarative usage with full type safety
function App() {
  return (
    <Menu>
      <Menu.Button>Toggle Navigation</Menu.Button>
      <Menu.List>
        <Menu.Item id="home">Home</Menu.Item>
        <Menu.Item id="about">About</Menu.Item>
        <Menu.Item id="services">Services</Menu.Item>
        <Menu.Item id="contact">Contact</Menu.Item>
      </Menu.List>
    </Menu>
  );
}

Key Improvements

The compound pattern version demonstrates several improvements:

  • No Prop Drilling: State is shared through context, eliminating prop passing
  • Declarative Syntax: The usage clearly expresses the component structure
  • Flexible Composition: Items can be arranged in any order or configuration
  • Implicit Communication: Components automatically stay in sync
  • Type Safety: Full TypeScript support with proper error handling

Advanced Compound Pattern Implementations

1. Flexible Dialog Component

Here's a more complex example showcasing the pattern's power with a dialog component:

import React, { 
  createContext, 
  useContext, 
  useState, 
  useEffect, 
  ReactNode, 
  MouseEvent,
  KeyboardEvent
} from 'react';

interface DialogContextType {
  isOpen: boolean;
  setIsOpen: (isOpen: boolean) => void;
  loading: boolean;
  setLoading: (loading: boolean) => void;
  open: () => void;
  close: () => void;
  showLoading: () => void;
  hideLoading: () => void;
}

const DialogContext = createContext<DialogContextType | undefined>(undefined);

function useDialogContext(): DialogContextType {
  const context = useContext(DialogContext);
  if (!context) {
    throw new Error('Dialog compound components must be used within Dialog');
  }
  return context;
}

interface DialogProps {
  children: ReactNode;
  onClose?: () => void;
}

function Dialog({ children, onClose }: DialogProps) {
  const [isOpen, setIsOpen] = useState(false);
  const [loading, setLoading] = useState(false);

  const contextValue: DialogContextType = {
    isOpen,
    setIsOpen,
    loading,
    setLoading,
    open: () => setIsOpen(true),
    close: () => {
      setIsOpen(false);
      onClose?.();
    },
    showLoading: () => setLoading(true),
    hideLoading: () => setLoading(false)
  };

  // Handle escape key
  useEffect(() => {
    const handleEscape = (e: KeyboardEvent<Document>) => {
      if (e.key === 'Escape' && isOpen) {
        contextValue.close();
      }
    };

    if (isOpen) {
      document.addEventListener('keydown', handleEscape as any);
      document.body.style.overflow = 'hidden';
    }

    return () => {
      document.removeEventListener('keydown', handleEscape as any);
      document.body.style.overflow = 'unset';
    };
  }, [isOpen, contextValue]);

  return (
    <DialogContext.Provider value={contextValue}>
      {children}
    </DialogContext.Provider>
  );
}

interface DialogTriggerProps {
  children: ReactNode;
  asChild?: boolean;
}

Dialog.Trigger = function DialogTrigger({ children, asChild = false }: DialogTriggerProps) {
  const { open } = useDialogContext();
  
  if (asChild && React.isValidElement(children)) {
    return React.cloneElement(children, { onClick: open });
  }
  
  return <button onClick={open}>{children}</button>;
};

interface DialogContentProps {
  children: ReactNode;
  className?: string;
}

Dialog.Content = function DialogContent({ children, className = '' }: DialogContentProps) {
  const { isOpen, close } = useDialogContext();
  
  if (!isOpen) return null;
  
  const handleOverlayClick = (e: MouseEvent<HTMLDivElement>) => {
    if (e.target === e.currentTarget) {
      close();
    }
  };
  
  return (
    <div className={`dialog-overlay ${className}`} onClick={handleOverlayClick}>
      <div className="dialog-content">
        {children}
      </div>
    </div>
  );
};

interface DialogHeaderProps {
  children: ReactNode;
}

Dialog.Header = function DialogHeader({ children }: DialogHeaderProps) {
  return <div className="dialog-header">{children}</div>;
};

interface DialogBodyProps {
  children: ReactNode;
}

Dialog.Body = function DialogBody({ children }: DialogBodyProps) {
  const { loading } = useDialogContext();
  
  return (
    <div className="dialog-body">
      {loading && <div className="loading-spinner">Loading...</div>}
      {children}
    </div>
  );
};

interface DialogFooterProps {
  children: ReactNode;
}

Dialog.Footer = function DialogFooter({ children }: DialogFooterProps) {
  return <div className="dialog-footer">{children}</div>;
};

interface DialogCloseProps {
  children?: ReactNode;
}

Dialog.Close = function DialogClose({ children }: DialogCloseProps) {
  const { close } = useDialogContext();
  
  return (
    <button className="dialog-close" onClick={close}>
      {children || '×'}
    </button>
  );
};

// Usage example with full type safety
function UserProfile() {
  const handleSave = async (): Promise<void> => {
    // Simulate API call
    await new Promise(resolve => setTimeout(resolve, 2000));
  };

  return (
    <Dialog onClose={() => console.log('Dialog closed')}>
      <Dialog.Trigger>
        <button className="primary-btn">Edit Profile</button>
      </Dialog.Trigger>
      
      <Dialog.Content>
        <Dialog.Header>
          <h2>Edit Profile</h2>
          <Dialog.Close />
        </Dialog.Header>
        
        <Dialog.Body>
          <form>
            <input type="text" placeholder="Name" />
            <input type="email" placeholder="Email" />
            <textarea placeholder="Bio"></textarea>
          </form>
        </Dialog.Body>
        
        <Dialog.Footer>
          <Dialog.Close>
            <button className="secondary-btn">Cancel</button>
          </Dialog.Close>
          <button className="primary-btn" onClick={handleSave}>
            Save Changes
          </button>
        </Dialog.Footer>
      </Dialog.Content>
    </Dialog>
  );
}

2. Advanced Data Table Component

For more complex scenarios, here's a data table implementation that showcases the pattern's scalability:

import React, { 
  createContext, 
  useContext, 
  useState, 
  useMemo, 
  ReactNode 
} from 'react';

// Generic type for table data
interface TableData {
  id: string | number;
  [key: string]: any;
}

type SortDirection = 'asc' | 'desc';

interface TableContextType<T extends TableData> {
  data: T[];
  originalData: T[];
  sortColumn: keyof T | null;
  sortDirection: SortDirection;
  filters: Record<string, string>;
  selectedRows: Set<string | number>;
  sort: (column: keyof T) => void;
  filter: (column: string, value: string) => void;
  selectRow: (id: string | number) => void;
  selectAllRows: () => void;
  clearSelection: () => void;
}

const TableContext = createContext<TableContextType<any> | undefined>(undefined);

function useTableContext<T extends TableData>(): TableContextType<T> {
  const context = useContext(TableContext);
  if (!context) {
    throw new Error('Table compound components must be used within Table');
  }
  return context;
}

interface TableProps<T extends TableData> {
  data: T[];
  children: ReactNode;
}

function Table<T extends TableData>({ data, children }: TableProps<T>) {
  const [sortColumn, setSortColumn] = useState<keyof T | null>(null);
  const [sortDirection, setSortDirection] = useState<SortDirection>('asc');
  const [filters, setFilters] = useState<Record<string, string>>({});
  const [selectedRows, setSelectedRows] = useState<Set<string | number>>(new Set());

  const processedData = useMemo(() => {
    let result = [...data];

    // Apply filters
    Object.entries(filters).forEach(([column, value]) => {
      if (value) {
        result = result.filter(row => 
          String(row[column]).toLowerCase().includes(value.toLowerCase())
        );
      }
    });

    // Apply sorting
    if (sortColumn) {
      result.sort((a, b) => {
        const aVal = a[sortColumn];
        const bVal = b[sortColumn];
        const modifier = sortDirection === 'asc' ? 1 : -1;
        
        if (aVal < bVal) return -1 * modifier;
        if (aVal > bVal) return 1 * modifier;
        return 0;
      });
    }

    return result;
  }, [data, filters, sortColumn, sortDirection]);

  const contextValue: TableContextType<T> = {
    data: processedData,
    originalData: data,
    sortColumn,
    sortDirection,
    filters,
    selectedRows,
    sort: (column: keyof T) => {
      if (sortColumn === column) {
        setSortDirection(prev => prev === 'asc' ? 'desc' : 'asc');
      } else {
        setSortColumn(column);
        setSortDirection('asc');
      }
    },
    filter: (column: string, value: string) => {
      setFilters(prev => ({ ...prev, [column]: value }));
    },
    selectRow: (id: string | number) => {
      setSelectedRows(prev => {
        const newSet = new Set(prev);
        if (newSet.has(id)) {
          newSet.delete(id);
        } else {
          newSet.add(id);
        }
        return newSet;
      });
    },
    selectAllRows: () => {
      setSelectedRows(new Set(processedData.map(row => row.id)));
    },
    clearSelection: () => {
      setSelectedRows(new Set());
    }
  };

  return (
    <TableContext.Provider value={contextValue}>
      <div className="table-container">
        {children}
      </div>
    </TableContext.Provider>
  );
}

interface TableHeaderProps {
  children: ReactNode;
}

Table.Header = function TableHeader({ children }: TableHeaderProps) {
  return <thead className="table-header">{children}</thead>;
};

interface TableBodyProps {
  children: ReactNode;
}

Table.Body = function TableBody({ children }: TableBodyProps) {
  const { data } = useTableContext();
  
  return (
    <tbody className="table-body">
      {data.map((row, index) => (
        <TableRowProvider key={row.id} row={row} index={index}>
          {children}
        </TableRowProvider>
      ))}
    </tbody>
  );
};

interface TableRowProps {
  children: ReactNode;
}

Table.Row = function TableRow({ children }: TableRowProps) {
  const { row, index } = useTableRowContext();
  const { selectedRows } = useTableContext();
  
  return (
    <tr 
      className={`table-row ${selectedRows.has(row.id) ? 'selected' : ''}`}
      data-index={index}
    >
      {children}
    </tr>
  );
};

interface TableHeaderCellProps {
  column?: string;
  children: ReactNode;
  sortable?: boolean;
}

Table.HeaderCell = function TableHeaderCell({ 
  column, 
  children, 
  sortable = false 
}: TableHeaderCellProps) {
  const { sort, sortColumn, sortDirection } = useTableContext();
  
  const handleClick = () => {
    if (sortable && column) {
      sort(column);
    }
  };

  return (
    <th 
      className={`table-header-cell ${sortable ? 'sortable' : ''}`}
      onClick={handleClick}
    >
      {children}
      {sortable && sortColumn === column && (
        <span className="sort-indicator">
          {sortDirection === 'asc' ? '↑' : '↓'}
        </span>
      )}
    </th>
  );
};

interface TableCellProps {
  column?: string;
  children?: ReactNode;
}

Table.Cell = function TableCell({ column, children }: TableCellProps) {
  const { row } = useTableRowContext();
  
  return (
    <td className="table-cell">
      {children || (column ? row[column] : '')}
    </td>
  );
};

Table.SelectCell = function TableSelectCell() {
  const { row } = useTableRowContext();
  const { selectedRows, selectRow } = useTableContext();
  
  return (
    <td className="table-select-cell">
      <input
        type="checkbox"
        checked={selectedRows.has(row.id)}
        onChange={() => selectRow(row.id)}
      />
    </td>
  );
};

interface TableFilterProps {
  column: string;
  placeholder?: string;
}

Table.Filter = function TableFilter({ column, placeholder }: TableFilterProps) {
  const { filters, filter } = useTableContext();
  
  return (
    <input
      type="text"
      placeholder={placeholder}
      value={filters[column] || ''}
      onChange={(e) => filter(column, e.target.value)}
      className="table-filter"
    />
  );
};

// Row context for accessing current row data
interface TableRowContextType<T extends TableData = TableData> {
  row: T;
  index: number;
}

const TableRowContext = createContext<TableRowContextType | undefined>(undefined);

interface TableRowProviderProps<T extends TableData> {
  row: T;
  index: number;
  children: ReactNode;
}

function TableRowProvider<T extends TableData>({ 
  row, 
  index, 
  children 
}: TableRowProviderProps<T>) {
  return (
    <TableRowContext.Provider value={{ row, index }}>
      {children}
    </TableRowContext.Provider>
  );
}

function useTableRowContext<T extends TableData = TableData>(): TableRowContextType<T> {
  const context = useContext(TableRowContext);
  if (!context) {
    throw new Error('Table row components must be used within Table.Body');
  }
  return context as TableRowContextType<T>;
}

// Usage example with proper typing
interface User {
  id: string;
  name: string;
  email: string;
  role: 'Admin' | 'User';
}

function UsersTable() {
  const users: User[] = [
    { id: '1', name: 'John Doe', email: 'john@example.com', role: 'Admin' },
    { id: '2', name: 'Jane Smith', email: 'jane@example.com', role: 'User' },
    { id: '3', name: 'Bob Johnson', email: 'bob@example.com', role: 'User' }
  ];

  return (
    <div>
      <div className="table-controls">
        <Table.Filter column="name" placeholder="Filter by name..." />
        <Table.Filter column="email" placeholder="Filter by email..." />
      </div>
      
      <table>
        <Table<User> data={users}>
          <Table.Header>
            <tr>
              <Table.HeaderCell>
                <input type="checkbox" />
              </Table.HeaderCell>
              <Table.HeaderCell column="name" sortable>
                Name
              </Table.HeaderCell>
              <Table.HeaderCell column="email" sortable>
                Email
              </Table.HeaderCell>
              <Table.HeaderCell column="role">
                Role
              </Table.HeaderCell>
            </tr>
          </Table.Header>
          
          <Table.Body>
            <Table.Row>
              <Table.SelectCell />
              <Table.Cell column="name" />
              <Table.Cell column="email" />
              <Table.Cell column="role">
                <span className="role-badge">
                  {/* Custom rendering with full type safety */}
                </span>
              </Table.Cell>
            </Table.Row>
          </Table.Body>
        </Table>
      </div>
    </div>
  );
}

Performance Considerations

While the compound pattern offers excellent developer experience, it's important to consider performance implications:

Context Performance Optimization

import React, { 
  createContext, 
  useContext, 
  useMemo, 
  useCallback, 
  useState,
  ReactNode 
} from 'react';

interface MenuState {
  isOpen: boolean;
  activeItem: string | null;
}

interface MenuActions {
  toggle: () => void;
  selectItem: (id: string) => void;
}

const MenuStateContext = createContext<MenuState | undefined>(undefined);
const MenuActionsContext = createContext<MenuActions | undefined>(undefined);

interface OptimizedMenuProps {
  children: ReactNode;
}

function OptimizedMenu({ children }: OptimizedMenuProps) {
  const [isOpen, setIsOpen] = useState(false);
  const [activeItem, setActiveItem] = useState<string | null>(null);

  // Separate state context (changes frequently)
  const stateContextValue = useMemo((): MenuState => ({
    isOpen,
    activeItem
  }), [isOpen, activeItem]);

  // Separate actions context (stable)
  const actionsContextValue = useMemo((): MenuActions => ({
    toggle: () => setIsOpen(prev => !prev),
    selectItem: (id: string) => setActiveItem(id)
  }), []);

  return (
    <MenuStateContext.Provider value={stateContextValue}>
      <MenuActionsContext.Provider value={actionsContextValue}>
        <div className="menu">{children}</div>
      </MenuActionsContext.Provider>
    </MenuStateContext.Provider>
  );
}

// Separate hooks for better performance
function useMenuState(): MenuState {
  const context = useContext(MenuStateContext);
  if (!context) {
    throw new Error('useMenuState must be used within OptimizedMenu');
  }
  return context;
}

function useMenuActions(): MenuActions {
  const context = useContext(MenuActionsContext);
  if (!context) {
    throw new Error('useMenuActions must be used within OptimizedMenu');
  }
  return context;
}

Best Practices for Performance

  1. Split Contexts: Separate frequently changing state from stable actions
  2. Memoize Context Values: Use useMemo to prevent unnecessary re-renders
  3. Optimize Child Components: Use React.memo for compound components when appropriate
  4. Lazy Evaluation: Only compute expensive values when needed
  5. Proper TypeScript: Use generics and proper typing for better performance and DX

When to Use the Compound Pattern

The compound pattern shines in specific scenarios:

Ideal Use Cases

  • Related Component Groups: When components naturally work together (tabs, accordion, dropdown)
  • Complex State Management: When multiple components need to share and synchronize state
  • Flexible APIs: When users need control over component arrangement and styling
  • Design Systems: When building reusable component libraries with TypeScript
  • Library Development: When creating components that need to be highly composable

When to Avoid

  • Simple Components: When prop drilling involves only one or two levels
  • Performance-Critical Paths: When context updates would cause expensive re-renders
  • Unrelated Components: When components don't naturally belong together
  • Over-Engineering: When simpler patterns would suffice

Comparison with Other Patterns

Compound Pattern vs. Render Props

// Render Props approach
interface MenuRenderProps {
  isOpen: boolean;
  setIsOpen: (isOpen: boolean) => void;
  toggle: () => void;
}

interface MenuWithRenderProps {
  children: (props: MenuRenderProps) => ReactNode;
}

function MenuWithRenderProps({ children }: MenuWithRenderProps) {
  const [isOpen, setIsOpen] = useState(false);
  
  return children({ 
    isOpen, 
    setIsOpen, 
    toggle: () => setIsOpen(!isOpen) 
  });
}

// Usage - less declarative
<MenuWithRenderProps>
  {({ isOpen, toggle }) => (
    <div>
      <button onClick={toggle}>Toggle</button>
      {isOpen && <ul>...</ul>}
    </div>
  )}
</MenuWithRenderProps>

// Compound Pattern is more declarative and readable
<Menu>
  <Menu.Button>Toggle</Menu.Button>
  <Menu.List>...</Menu.List>
</Menu>

Compound Pattern vs. HOCs

The compound pattern offers better composition and avoids the wrapper hell often associated with Higher-Order Components while providing excellent TypeScript support and better inference.

Extending the pattern: beyond composition

Once you've grasped the power of the Compound Pattern, it's time to push beyond. Here are some advanced techniques that can transform your components from simple building blocks into true user interface ecosystems.

Auto-Detection of Child Components

Sometimes exposing a declarative API isn't enough, we also need to be smart about how our components behave based on their structure. Using React.Children and traversal functions, we can create components that automatically adapt to their content.

import React, { ReactNode, ReactElement, Children, isValidElement } from 'react';

interface TabsProps {
  children: ReactNode;
  defaultValue?: string;
}

function Tabs({ children, defaultValue }: TabsProps) {
  const [activeTab, setActiveTab] = useState<string>(() => {
    // Auto-detect first tab if no default provided
    if (defaultValue) return defaultValue;
    
    const tabList = Children.toArray(children).find(
      (child): child is ReactElement => 
        isValidElement(child) && child.type === Tabs.List
    );
    
    if (tabList) {
      const firstTrigger = Children.toArray(tabList.props.children).find(
        (child): child is ReactElement => 
          isValidElement(child) && child.type === Tabs.Trigger
      );
      
      return firstTrigger?.props.value || '';
    }
    
    return '';
  });

  const contextValue = {
    activeTab,
    setActiveTab,
    // Auto-detect available tabs
    availableTabs: useMemo(() => {
      const tabs: string[] = [];
      
      Children.forEach(children, (child) => {
        if (isValidElement(child) && child.type === Tabs.Content) {
          tabs.push(child.props.value);
        }
      });
      
      return tabs;
    }, [children])
  };

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

Tabs.List = function TabsList({ children }: { children: ReactNode }) {
  const { availableTabs } = useTabsContext();
  
  // Auto-filter only valid triggers
  const validTriggers = Children.toArray(children).filter(
    (child): child is ReactElement => 
      isValidElement(child) && 
      child.type === Tabs.Trigger &&
      availableTabs.includes(child.props.value)
  );

  return (
    <div className="tabs-list" role="tablist">
      {validTriggers}
    </div>
  );
};

Tabs.Trigger = function TabsTrigger({ 
  value,
  children 
}: { 
  value: string; 
  children: ReactNode 
}) {
  const { activeTab, setActiveTab } = useTabsContext();
  
  return (
    <button
      role="tab"
      aria-selected={activeTab === value}
      className={`tabs-trigger ${activeTab === value ? 'active' : ''}`}
      onClick={() => setActiveTab(value)}
    >
      {children}
    </button>
  );
};

Tabs.Content = function TabsContent({ 
  value, 
  children 
}: { 
  value: string; 
  children: ReactNode 
}) {
  const { activeTab } = useTabsContext();
  
  if (activeTab !== value) return null;
  
  return (
    <div role="tabpanel" className="tabs-content">
      {children}
    </div>
  );
};

// Usage with auto-detection
function SmartTabs() {
  return (
    <Tabs defaultValue="overview">
      <Tabs.List>
        <Tabs.Trigger value="overview">Overview</Tabs.Trigger>
        <Tabs.Trigger value="settings">Settings</Tabs.Trigger>
        <Tabs.Trigger value="invalid">Invalid</Tabs.Trigger> {/* Automatically filtered */}
      </Tabs.List>
      
      <Tabs.Content value="overview">
        <h2>Overview Content</h2>
      </Tabs.Content>
      
      <Tabs.Content value="settings">
        <h2>Settings Content</h2>
      </Tabs.Content>
      
      {/* The "invalid" trigger has no corresponding content */}
    </Tabs>
  );
}

Context Overrides at Intermediate Levels

One of the most powerful patterns is the ability to override compound component behavior at intermediate levels. This allows you to create nested components that can modify parent behavior without breaking the communication chain.

interface NestedMenuContextType extends MenuContextType {
  level: number;
  parentContext?: MenuContextType;
}

const NestedMenuContext = createContext<NestedMenuContextType | undefined>(undefined);

interface SubmenuProps {
  children: ReactNode;
  trigger: ReactNode;
  disabled?: boolean;
}

function Submenu({ children, trigger, disabled = false }: SubmenuProps) {
  const parentContext = useMenuContext();
  const [isOpen, setIsOpen] = useState(false);
  
  // Override specific behaviors while preserving parent context
  const nestedContextValue: NestedMenuContextType = {
    ...parentContext,
    level: (parentContext as any).level + 1 || 1,
    parentContext,
    isOpen,
    setIsOpen,
    toggle: () => !disabled && setIsOpen(!isOpen),
    // Preserve parent's active item logic
    selectItem: (id: string) => {
      parentContext.selectItem(id);
      setIsOpen(false); // Close submenu when item is selected
    }
  };

  return (
    <NestedMenuContext.Provider value={nestedContextValue}>
      <div className={`submenu level-${nestedContextValue.level}`}>
        <div 
          className={`submenu-trigger ${disabled ? 'disabled' : ''}`}
          onClick={nestedContextValue.toggle}
        >
          {trigger}
        </div>
        
        {isOpen && (
          <div className="submenu-content">
            {children}
          </div>
        )}
      </div>
    </NestedMenuContext.Provider>
  );
}

// Hook that works with both normal and nested context
function useMenuOrNestedContext(): MenuContextType | NestedMenuContextType {
  const nestedContext = useContext(NestedMenuContext);
  const menuContext = useMenuContext();
  
  return nestedContext || menuContext;
}

// Usage with nested overrides
function AdvancedMenu() {
  return (
    <Menu>
      <Menu.Button>Main Menu</Menu.Button>
      <Menu.List>
        <Menu.Item id="home">Home</Menu.Item>
        
        <Submenu trigger={<span>Products ▶</span>}>
          <Menu.Item id="product-1">Product 1</Menu.Item>
          <Menu.Item id="product-2">Product 2</Menu.Item>
          
          <Submenu trigger={<span>Categories ▶</span>}>
            <Menu.Item id="cat-1">Category 1</Menu.Item>
            <Menu.Item id="cat-2">Category 2</Menu.Item>
          </Submenu>
        </Submenu>
        
        <Menu.Item id="about">About</Menu.Item>
      </Menu.List>
    </Menu>
  );
}

Dynamic Slots: The ShadCN/Radix Pattern

The true power of the compound pattern emerges when we implement dynamic slots, allowing components to adapt and reorganize based on content. This is the secret behind the flexibility of libraries like Radix UI and ShadCN.

interface SlotProps {
  children?: ReactNode;
  asChild?: boolean;
}

// Primitive Slot component inspired by Radix
function Slot({ children, asChild = false, ...props }: SlotProps & any) {
  if (asChild && React.isValidElement(children)) {
    return React.cloneElement(children, {
      ...props,
      ...children.props,
      className: `${props.className || ''} ${children.props.className || ''}`.trim()
    });
  }
  
  return React.createElement('div', props, children);
}

// Advanced Dialog with dynamic slots
interface DialogWithSlotsProps {
  children: ReactNode;
  modal?: boolean;
  onOpenChange?: (open: boolean) => void;
}

function DialogWithSlots({ children, modal = true, onOpenChange }: DialogWithSlotsProps) {
  const [isOpen, setIsOpen] = useState(false);
  
  const handleOpenChange = (open: boolean) => {
    setIsOpen(open);
    onOpenChange?.(open);
  };

  // Detect and organize slots automatically
  const slots = useMemo(() => {
    const result: {
      trigger?: ReactElement;
      content?: ReactElement;
      overlay?: ReactElement;
      portal?: ReactElement;
    } = {};
    
    Children.forEach(children, (child) => {
      if (isValidElement(child)) {
        const displayName = (child.type as any).displayName;
        
        switch (displayName) {
          case 'DialogTrigger':
            result.trigger = child;
            break;
          case 'DialogContent':
            result.content = child;
            break;
          case 'DialogOverlay':
            result.overlay = child;
            break;
          case 'DialogPortal':
            result.portal = child;
            break;
        }
      }
    });
    
    return result;
  }, [children]);

  const contextValue = {
    isOpen,
    onOpenChange: handleOpenChange,
    modal,
    slots
  };

  return (
    <DialogContext.Provider value={contextValue}>
      {slots.trigger}
      {isOpen && (
        modal ? (
          <DialogPortal>
            {slots.overlay}
            {slots.content}
          </DialogPortal>
        ) : (
          <>
            {slots.overlay}
            {slots.content}
          </>
        )
      )}
    </DialogContext.Provider>
  );
}

// Dynamic slot components
interface DialogTriggerProps {
  children: ReactNode;
  asChild?: boolean;
}

DialogWithSlots.Trigger = function DialogTrigger({ 
  children, 
  asChild = false 
}: DialogTriggerProps) {
  const { onOpenChange } = useDialogContext();
  
  const handleClick = () => onOpenChange(true);
  
  return (
    <Slot asChild={asChild} onClick={handleClick}>
      {children}
    </Slot>
  );
};

DialogWithSlots.Trigger.displayName = 'DialogTrigger';

interface DialogContentProps {
  children: ReactNode;
  className?: string;
  onPointerDownOutside?: (event: Event) => void;
  onEscapeKeyDown?: (event: KeyboardEvent) => void;
}

DialogWithSlots.Content = function DialogContent({
  children,
  className,
  onPointerDownOutside,
  onEscapeKeyDown
}: DialogContentProps) {
  const { isOpen, onOpenChange, modal } = useDialogContext();
  
  useEffect(() => {
    const handleEscape = (e: KeyboardEvent) => {
      if (e.key === 'Escape') {
        onEscapeKeyDown?.(e);
        if (!e.defaultPrevented) {
          onOpenChange(false);
        }
      }
    };

    if (isOpen) {
      document.addEventListener('keydown', handleEscape);
    }

    return () => {
      document.removeEventListener('keydown', handleEscape);
    };
  }, [isOpen, onOpenChange, onEscapeKeyDown]);

  const handlePointerDownOutside = (e: Event) => {
    onPointerDownOutside?.(e);
    if (!e.defaultPrevented && modal) {
      onOpenChange(false);
    }
  };

  return (
    <div
      className={`dialog-content ${className || ''}`}
      onPointerDown={(e) => {
        if (e.target === e.currentTarget) {
          handlePointerDownOutside(e.nativeEvent);
        }
      }}
    >
      {children}
    </div>
  );
};

DialogWithSlots.Content.displayName = 'DialogContent';

// Portal component for rendering outside DOM hierarchy
interface DialogPortalProps {
  children: ReactNode;
  container?: Element;
}

DialogWithSlots.Portal = function DialogPortal({ 
  children, 
  container 
}: DialogPortalProps) {
  const [mounted, setMounted] = useState(false);
  
  useEffect(() => {
    setMounted(true);
  }, []);

  if (!mounted) return null;

  return ReactDOM.createPortal(
    children,
    container || document.body
  );
};

DialogWithSlots.Portal.displayName = 'DialogPortal';

// Overlay component
interface DialogOverlayProps {
  className?: string;
  children?: ReactNode;
}

DialogWithSlots.Overlay = function DialogOverlay({ 
  className,
  children 
}: DialogOverlayProps) {
  return (
    <div className={`dialog-overlay ${className || ''}`}>
      {children}
    </div>
  );
};

DialogWithSlots.Overlay.displayName = 'DialogOverlay';

// Advanced usage with complete flexibility
function FlexibleDialog() {
  return (
    <DialogWithSlots modal onOpenChange={(open) => console.log('Dialog:', open)}>
      <DialogWithSlots.Trigger asChild>
        <button className="custom-trigger">Open Advanced Dialog</button>
      </DialogWithSlots.Trigger>
      
      <DialogWithSlots.Portal>
        <DialogWithSlots.Overlay className="custom-overlay" />
        <DialogWithSlots.Content 
          className="custom-content"
          onEscapeKeyDown={(e) => {
            console.log('Escape pressed');
            // Could prevent default to keep dialog open
          }}
          onPointerDownOutside={(e) => {
            console.log('Clicked outside');
            // Could prevent default to keep dialog open
          }}
        >
          <h2>Advanced Dialog</h2>
          <p>This dialog uses dynamic slots and advanced composition patterns.</p>
          
          <DialogWithSlots.Trigger asChild>
            <button>Close</button>
          </DialogWithSlots.Trigger>
        </DialogWithSlots.Content>
      </DialogWithSlots.Portal>
    </DialogWithSlots>
  );
}

Conclusion and Recommendations

The Compound Pattern represents more than just a composition technique, it's a design philosophy that centers the developer experience and API flexibility. When fully mastered, it allows you to create components that are not only powerful but also delightful to use.

For Type Safety:

  • Use generics to make your components reusable
  • Implement proper error boundaries and validation
  • Leverage TypeScript to create APIs that naturally guide correct usage

For Performance:

  • Split contexts when state and actions update at different frequencies
  • Use useMemo and useCallback strategically
  • Consider the impact of re-renders on complex component trees

For Architecture:

  • Apply the pattern to naturally related component groups
  • Maintain clear separation between state management and rendering logic
  • Design for composition, not configuration

For Extensibility:

  • Implement dynamic slots for maximum flexibility
  • Allow context overrides at intermediate levels
  • Create custom hooks for complex behaviors

The compound pattern transforms how components feel to use. Not just functional, but intuitive. Not just flexible, but inevitable.

You'll know you've got it right when someone extends your component months later and the solution feels obvious, when complex UIs emerge from simple composition. That's the mark of truly great component design.

Additional Resources

Happy coding!