Component Composition Patterns in React

Module 25: Frontend Frameworks & State Management

Introduction to Component Composition

Component composition is the process of combining smaller, more focused components to build complex user interfaces. It's one of React's fundamental design principles and key to building maintainable applications.

Think of component composition like building with LEGO blocks – each piece has a specific purpose, and by connecting them in various ways, you can create complex structures. Similarly, in React, we compose small, focused components to build sophisticated applications.

graph TD A[App] --> B[Header] A --> C[Main Content] A --> D[Footer] C --> E[Sidebar] C --> F[Article List] F --> G[Article Item] G --> H[Comments] H --> I[Comment Item]

Why Component Composition Matters

Composition offers several key advantages over other approaches like inheritance:

Real-world analogy: Consider a car manufacturing plant. Rather than building an entire car in one process, different teams create specialized parts (engine, transmission, body panels) that are then assembled. This allows for specialization, quality testing at each stage, and the ability to use the same parts across different car models.

Containment Pattern

The containment pattern allows components to accept and render children, much like HTML elements. This pattern is implemented using the children prop in React.

Example: A Card Component

Let's create a reusable Card component that can wrap any content:

// Card.jsx
function Card({ children, title }) {
  return (
    <div className="card">
      {title && <div className="card-header">{title}</div>}
      <div className="card-body">
        {children}
      </div>
    </div>
  );
}

// Usage
function App() {
  return (
    <div>
      <Card title="User Profile">
        <h3>Jane Doe</h3>
        <p>Software Engineer</p>
        <button>View Profile</button>
      </Card>
      
      <Card title="Weather">
        <h3>San Francisco</h3>
        <p>68°F, Partly Cloudy</p>
      </Card>
    </div>
  );
}

The Card component is responsible for the styling and structure, while the content remains flexible and determined by the parent component.

graph TD A[App] --> B[Card: User Profile] A --> C[Card: Weather] B --> D[h3: Jane Doe] B --> E[p: Software Engineer] B --> F[button: View Profile] C --> G[h3: San Francisco] C --> H[p: 68°F, Partly Cloudy]

Real-world application: UI component libraries like Material-UI, Chakra UI, and Ant Design all use the containment pattern extensively for their container components like cards, modals, and layout components.

Specialized Components Pattern

In this pattern, we create a collection of related components that work together, with one primary parent component and specialized child components.

Example: A Tabs Component

// Tabs component family
const TabsContext = React.createContext();

function Tabs({ children, defaultTab }) {
  const [activeTab, setActiveTab] = React.useState(defaultTab);
  
  return (
    <TabsContext.Provider value={{ activeTab, setActiveTab }}>
      <div className="tabs-container">
        {children}
      </div>
    </TabsContext.Provider>
  );
}

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

function Tab({ children, value }) {
  const { activeTab, setActiveTab } = React.useContext(TabsContext);
  
  return (
    <button 
      className={`tab ${activeTab === value ? 'active' : ''}`}
      onClick={() => setActiveTab(value)}
    >
      {children}
    </button>
  );
}

function TabPanel({ children, value }) {
  const { activeTab } = React.useContext(TabsContext);
  
  if (value !== activeTab) return null;
  
  return <div className="tab-panel">{children}</div>;
}

// Make them available as properties of the parent
Tabs.TabList = TabList;
Tabs.Tab = Tab;
Tabs.TabPanel = TabPanel;

// Usage
function App() {
  return (
    <Tabs defaultTab="profile">
      <Tabs.TabList>
        <Tabs.Tab value="profile">Profile</Tabs.Tab>
        <Tabs.Tab value="settings">Settings</Tabs.Tab>
        <Tabs.Tab value="notifications">Notifications</Tabs.Tab>
      </Tabs.TabList>
      
      <Tabs.TabPanel value="profile">
        <h3>User Profile</h3>
        {/* Profile content */}
      </Tabs.TabPanel>
      
      <Tabs.TabPanel value="settings">
        <h3>Settings</h3>
        {/* Settings content */}
      </Tabs.TabPanel>
      
      <Tabs.TabPanel value="notifications">
        <h3>Notifications</h3>
        {/* Notifications content */}
      </Tabs.TabPanel>
    </Tabs>
  );
}

This pattern creates a cohesive API for a complex component by defining a clear parent-child relationship between components that are meant to be used together.

Real-world examples: Libraries like Reach UI, Headless UI, and Radix UI use this pattern extensively to create accessible compound components.

Practice Activity

Build a Modal Component with Composition

Create a Modal component that uses the containment pattern to allow flexible content. The Modal should have:

  1. A backdrop that covers the screen
  2. A modal container with customizable content
  3. Open/close functionality
  4. Optional header and footer sections

Start with this skeleton:

function Modal({ isOpen, onClose, children }) {
  if (!isOpen) return null;
  
  return (
    <div className="modal-backdrop" onClick={onClose}>
      <div 
        className="modal-content" 
        onClick={(e) => e.stopPropagation()}
      >
        {children}
      </div>
    </div>
  );
}

// Now extend this to support Modal.Header and Modal.Footer components

Challenge: Refactor the Modal to use the specialized components pattern to create a Modal.Header, Modal.Body, and Modal.Footer component family.

Key Takeaways

Additional Resources