Episode 2 — React Frontend Architecture NextJS / 2.6 — Component Architecture Principles

2.6.e — Component Composition

In one sentence: Composition is React's primary mechanism for building complex UIs from simple building blocks — using children, render props, and slots instead of inheritance to create flexible, scalable component systems.

Navigation: ← Prop Drilling Problem · Next → 2.7 Useful Hooks in React


Table of Contents

  1. What Is Composition?
  2. Composition vs Inheritance
  3. The children Prop
  4. Slot Pattern (Named Slots)
  5. Specialization Pattern
  6. Compound Components Pattern
  7. Render Props Pattern
  8. Building a Layout System
  9. Building a Card System
  10. Building a Modal System
  11. Composition for Prop Drilling Solutions
  12. Composition with TypeScript
  13. When Composition Goes Wrong
  14. Real-World Examples from Popular Libraries
  15. Key Takeaways

1. What Is Composition?

Composition means building complex things from simple, independent parts. In React, this translates to building complex components by combining smaller, simpler components.

Composition in daily life:
  LEGO bricks → castle
  Musical notes → symphony  
  Words → sentences → paragraphs → story

Composition in React:
  Button + Input + Label → FormField
  FormField + FormField + Submit → LoginForm
  Header + Sidebar + Content → PageLayout
  PageLayout + LoginForm → LoginPage

The React Team's Recommendation

From the official React docs:

"At Facebook, we use React in thousands of components, and we haven't found any use cases where we would recommend creating component inheritance hierarchies. Props and composition give you all the flexibility you need."

Two Axes of Composition

┌────────────────────────────────────────────────────────┐
│                                                         │
│    CONTAINMENT                    SPECIALIZATION        │
│    "I don't know what            "I'm a specific        │
│     goes inside me"               version of a          │
│                                   general component"    │
│    ┌──────────────┐              ┌──────────────────┐  │
│    │    Card       │              │   AlertCard      │  │
│    │  ┌────────┐  │              │ = Card + red bg  │  │
│    │  │children │  │              │   + icon         │  │
│    │  │ (???)   │  │              │   + dismiss btn  │  │
│    │  └────────┘  │              └──────────────────┘  │
│    └──────────────┘                                    │
│                                                         │
│    Modal, Layout,                 SuccessAlert,          │
│    Sidebar, Grid                  PrimaryButton,         │
│                                   AdminDashboard         │
└────────────────────────────────────────────────────────┘

2. Composition vs Inheritance

Why React Doesn't Use Inheritance

In classical OOP, you'd extend base classes:

// ❌ OOP inheritance approach (NOT how React works)
class Component {}
class Button extends Component {}
class IconButton extends Button {}
class DangerIconButton extends IconButton {}
// 4-level hierarchy — fragile, rigid, hard to change

Problems with inheritance:

  1. Fragile base class — changing Button breaks IconButton and DangerIconButton
  2. Diamond problem — what if you need IconButton AND ToggleButton?
  3. Rigid hierarchy — can't easily rearrange
  4. Deep coupling — child knows parent's implementation details

Composition Approach

// ✅ React composition approach
function Button({ children, variant = 'default', ...props }) {
  return <button className={`btn btn-${variant}`} {...props}>{children}</button>;
}

function IconButton({ icon, children, ...props }) {
  return (
    <Button {...props}>
      <span className="icon">{icon}</span>
      {children}
    </Button>
  );
}

// No class hierarchy — just wrapping
<Button>Click me</Button>
<Button variant="danger">Delete</Button>
<IconButton icon="🗑️" variant="danger">Delete</IconButton>

Comparison Table

AspectInheritanceComposition
FlexibilityLow — fixed hierarchyHigh — mix and match
CouplingTight — child knows parentLoose — connected via props
ReuseExtend or overrideWrap or combine
RefactoringDangerous — breaks childrenSafe — each piece independent
Mental model"Is-a" relationship"Has-a" / "Uses-a" relationship
React recommendation❌ Not recommended✅ Primary pattern

3. The children Prop

children is React's built-in composition mechanism. Any JSX nested between a component's opening and closing tags becomes the children prop.

Basics

// children is implicit — whatever goes between tags
function Card({ children }) {
  return <div className="card">{children}</div>;
}

// Usage
<Card>
  <h2>Title</h2>
  <p>Content goes here</p>
</Card>

// Equivalent to:
<Card children={<><h2>Title</h2><p>Content goes here</p></>} />

What children Can Be

// String
<Card>Hello world</Card>                    // children = "Hello world"

// Single element
<Card><p>Paragraph</p></Card>               // children = <p> element

// Multiple elements
<Card><h2>Title</h2><p>Body</p></Card>      // children = array of elements

// Component
<Card><UserProfile user={user} /></Card>    // children = component element

// Function (render prop pattern)
<Card>{(isOpen) => isOpen ? 'Open' : 'Closed'}</Card>

// Nothing
<Card />                                     // children = undefined
<Card></Card>                                // children = undefined

Manipulating children

import { Children, cloneElement } from 'react';

// Counting children
function TabBar({ children }) {
  const count = Children.count(children);
  return <div>Tabs: {count}</div>;
}

// Iterating children
function List({ children }) {
  return (
    <ul>
      {Children.map(children, (child, index) => (
        <li key={index}>{child}</li>
      ))}
    </ul>
  );
}

// Injecting props into children
function Toolbar({ children, disabled }) {
  return (
    <div className="toolbar">
      {Children.map(children, child =>
        cloneElement(child, { disabled })
      )}
    </div>
  );
}

// Usage — all buttons get disabled={true}
<Toolbar disabled={true}>
  <Button>Save</Button>
  <Button>Cancel</Button>
  <Button>Delete</Button>
</Toolbar>

Conditional children Rendering

function ConditionalWrapper({ condition, wrapper, children }) {
  return condition ? wrapper(children) : children;
}

// Usage — only wraps in <a> if there's a link
<ConditionalWrapper
  condition={!!link}
  wrapper={children => <a href={link}>{children}</a>}
>
  <Card>Content</Card>
</ConditionalWrapper>

4. Slot Pattern (Named Slots)

When children isn't enough — you need multiple insertion points.

The Problem

// children is ONE slot — but a dialog needs header, body, AND footer
<Dialog>
  {/* Everything goes into one place — can't separate */}
</Dialog>

Named Slots via Props

function Dialog({ title, children, footer }) {
  return (
    <div className="dialog-overlay">
      <div className="dialog">
        <div className="dialog-header">
          <h2>{title}</h2>
        </div>
        <div className="dialog-body">
          {children}
        </div>
        {footer && (
          <div className="dialog-footer">
            {footer}
          </div>
        )}
      </div>
    </div>
  );
}

// Usage — each slot gets its own content
<Dialog
  title="Confirm Delete"
  footer={
    <div className="flex gap-2">
      <Button variant="secondary" onClick={onCancel}>Cancel</Button>
      <Button variant="danger" onClick={onDelete}>Delete</Button>
    </div>
  }
>
  <p>Are you sure you want to delete this item?</p>
  <p className="text-sm text-gray-500">This action cannot be undone.</p>
</Dialog>

Advanced Multi-Slot Layout

function PageLayout({ header, sidebar, footer, children }) {
  return (
    <div className="page-layout">
      {header && <header className="page-header">{header}</header>}
      <div className="page-body">
        {sidebar && <aside className="page-sidebar">{sidebar}</aside>}
        <main className="page-content">{children}</main>
      </div>
      {footer && <footer className="page-footer">{footer}</footer>}
    </div>
  );
}

// Usage
<PageLayout
  header={<TopNav />}
  sidebar={<SideMenu items={menuItems} />}
  footer={<FooterLinks />}
>
  <h1>Dashboard</h1>
  <DashboardContent />
</PageLayout>

Slot Pattern Diagram

┌─────────────────────────────────────────┐
│               PageLayout                 │
│                                          │
│  ┌──────────────────────────────────┐   │
│  │         header slot               │   │
│  │    <TopNav />                     │   │
│  └──────────────────────────────────┘   │
│                                          │
│  ┌──────────┐  ┌────────────────────┐   │
│  │ sidebar  │  │    children        │   │
│  │  slot    │  │    (main content)  │   │
│  │          │  │                    │   │
│  │ <Side    │  │  <h1>Dashboard</h1>│   │
│  │  Menu /> │  │  <DashboardContent>│   │
│  │          │  │                    │   │
│  └──────────┘  └────────────────────┘   │
│                                          │
│  ┌──────────────────────────────────┐   │
│  │         footer slot               │   │
│  │    <FooterLinks />                │   │
│  └──────────────────────────────────┘   │
│                                          │
└─────────────────────────────────────────┘

5. Specialization Pattern

Creating specific components from generic ones by pre-filling props.

// Generic component
function Alert({ type, icon, title, children, onDismiss }) {
  const styles = {
    success: 'bg-green-50 border-green-500 text-green-800',
    error: 'bg-red-50 border-red-500 text-red-800',
    warning: 'bg-yellow-50 border-yellow-500 text-yellow-800',
    info: 'bg-blue-50 border-blue-500 text-blue-800',
  };

  return (
    <div className={`alert border-l-4 p-4 ${styles[type]}`}>
      <div className="flex items-center gap-2">
        {icon && <span className="text-xl">{icon}</span>}
        {title && <strong>{title}</strong>}
        {onDismiss && (
          <button onClick={onDismiss} className="ml-auto">×</button>
        )}
      </div>
      <div>{children}</div>
    </div>
  );
}

// Specialized versions — pre-fill type and icon
function SuccessAlert({ title, children, ...props }) {
  return <Alert type="success" icon="✅" title={title} {...props}>{children}</Alert>;
}

function ErrorAlert({ title, children, ...props }) {
  return <Alert type="error" icon="❌" title={title} {...props}>{children}</Alert>;
}

function WarningAlert({ title, children, ...props }) {
  return <Alert type="warning" icon="⚠️" title={title} {...props}>{children}</Alert>;
}

// Usage — clean and semantic
<SuccessAlert title="Saved!">Your changes have been saved.</SuccessAlert>
<ErrorAlert title="Error" onDismiss={handleDismiss}>Something went wrong.</ErrorAlert>

More Complex Specialization

// Generic DataTable
function DataTable({ columns, data, onSort, onRowClick, emptyMessage, ...props }) {
  // Full featured table implementation
}

// Specialized: UserTable pre-fills columns and formatting
function UserTable({ users, onUserClick }) {
  const columns = [
    { key: 'name', label: 'Name', render: u => <UserNameCell user={u} /> },
    { key: 'email', label: 'Email' },
    { key: 'role', label: 'Role', render: u => <RoleBadge role={u.role} /> },
    { key: 'status', label: 'Status', render: u => <StatusDot status={u.status} /> },
  ];

  return (
    <DataTable
      columns={columns}
      data={users}
      onRowClick={onUserClick}
      emptyMessage="No users found"
    />
  );
}

// Specialized: OrderTable — different columns, same base table
function OrderTable({ orders, onOrderClick }) {
  const columns = [
    { key: 'id', label: 'Order #' },
    { key: 'customer', label: 'Customer' },
    { key: 'total', label: 'Total', render: o => `$${o.total.toFixed(2)}` },
    { key: 'status', label: 'Status', render: o => <OrderStatusBadge status={o.status} /> },
  ];

  return (
    <DataTable
      columns={columns}
      data={orders}
      onRowClick={onOrderClick}
      emptyMessage="No orders yet"
    />
  );
}

6. Compound Components Pattern

Components that work together, sharing implicit state. Like <select> + <option> — they belong together.

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

// Shared context for the compound component
const AccordionContext = createContext();

// Parent component — manages state
function Accordion({ children, allowMultiple = false }) {
  const [openItems, setOpenItems] = useState(new Set());

  const toggle = (id) => {
    setOpenItems(prev => {
      const next = new Set(prev);
      if (next.has(id)) {
        next.delete(id);
      } else {
        if (!allowMultiple) next.clear();
        next.add(id);
      }
      return next;
    });
  };

  const isOpen = (id) => openItems.has(id);

  return (
    <AccordionContext.Provider value={{ toggle, isOpen }}>
      <div className="accordion">{children}</div>
    </AccordionContext.Provider>
  );
}

// Child component — reads from shared context
function AccordionItem({ id, title, children }) {
  const { toggle, isOpen } = useContext(AccordionContext);
  const open = isOpen(id);

  return (
    <div className="accordion-item">
      <button
        className="accordion-trigger"
        onClick={() => toggle(id)}
        aria-expanded={open}
      >
        {title}
        <span className={`arrow ${open ? 'up' : 'down'}`}></span>
      </button>
      {open && <div className="accordion-content">{children}</div>}
    </div>
  );
}

// Attach sub-components
Accordion.Item = AccordionItem;

// Usage — clean, declarative API
<Accordion allowMultiple>
  <Accordion.Item id="basics" title="What is React?">
    <p>React is a JavaScript library for building user interfaces.</p>
  </Accordion.Item>
  <Accordion.Item id="hooks" title="What are Hooks?">
    <p>Hooks are functions that let you use state in functional components.</p>
  </Accordion.Item>
  <Accordion.Item id="next" title="What is Next.js?">
    <p>Next.js is a React framework for production applications.</p>
  </Accordion.Item>
</Accordion>

Another Example: Tabs

const TabsContext = createContext();

function Tabs({ defaultValue, children }) {
  const [activeTab, setActiveTab] = useState(defaultValue);

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

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

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

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

Tabs.List = TabList;
Tabs.Tab = Tab;
Tabs.Panel = TabPanel;

// Usage
<Tabs defaultValue="code">
  <Tabs.List>
    <Tabs.Tab value="code">Code</Tabs.Tab>
    <Tabs.Tab value="preview">Preview</Tabs.Tab>
    <Tabs.Tab value="tests">Tests</Tabs.Tab>
  </Tabs.List>
  <Tabs.Panel value="code"><CodeEditor /></Tabs.Panel>
  <Tabs.Panel value="preview"><LivePreview /></Tabs.Panel>
  <Tabs.Panel value="tests"><TestRunner /></Tabs.Panel>
</Tabs>

7. Render Props Pattern

Passing a function as a prop that returns JSX. The component provides data; the consumer decides the UI.

// Component provides mouse position data
function MouseTracker({ children }) {
  const [position, setPosition] = useState({ x: 0, y: 0 });

  const handleMouseMove = (e) => {
    setPosition({ x: e.clientX, y: e.clientY });
  };

  return (
    <div onMouseMove={handleMouseMove} style={{ height: '100%' }}>
      {children(position)}
    </div>
  );
}

// Consumer decides how to render
<MouseTracker>
  {({ x, y }) => (
    <div>
      <p>Mouse: ({x}, {y})</p>
      <div style={{
        position: 'absolute',
        left: x - 10,
        top: y - 10,
        width: 20,
        height: 20,
        borderRadius: '50%',
        background: 'red'
      }} />
    </div>
  )}
</MouseTracker>

Practical Example: Fetch with Render Props

function FetchData({ url, children }) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    setLoading(true);
    fetch(url)
      .then(r => r.json())
      .then(setData)
      .catch(setError)
      .finally(() => setLoading(false));
  }, [url]);

  return children({ data, loading, error });
}

// Usage — consumer decides the UI for each state
<FetchData url="/api/users">
  {({ data, loading, error }) => {
    if (loading) return <Spinner />;
    if (error) return <ErrorMessage error={error} />;
    return <UserList users={data} />;
  }}
</FetchData>

Render Props vs Custom Hooks

// Render prop approach
<FetchData url="/api/users">
  {({ data, loading }) => loading ? <Spinner /> : <UserList users={data} />}
</FetchData>

// Custom hook approach (preferred in modern React)
function UserListPage() {
  const { data, loading } = useFetch('/api/users');
  if (loading) return <Spinner />;
  return <UserList users={data} />;
}

Custom hooks are simpler in most cases. Render props still shine for component libraries where you want to provide headless behavior with consumer-defined UI.


8. Building a Layout System

Composition is perfect for building flexible, reusable layout systems.

// === ATOMIC LAYOUT COMPONENTS ===

function Stack({ children, gap = 4, direction = 'vertical', align, justify, className = '' }) {
  const dirClass = direction === 'horizontal' ? 'flex-row' : 'flex-col';
  return (
    <div className={`flex ${dirClass} gap-${gap} ${align ? `items-${align}` : ''} 
      ${justify ? `justify-${justify}` : ''} ${className}`}>
      {children}
    </div>
  );
}

function Cluster({ children, gap = 4, justify = 'start', wrap = true, className = '' }) {
  return (
    <div className={`flex ${wrap ? 'flex-wrap' : ''} gap-${gap} 
      justify-${justify} ${className}`}>
      {children}
    </div>
  );
}

function Center({ children, max = '7xl', className = '' }) {
  return (
    <div className={`max-w-${max} mx-auto px-4 ${className}`}>
      {children}
    </div>
  );
}

function Sidebar({ children, side = 'left', sideWidth = '250px', gap = 4, className = '' }) {
  return (
    <div className={`flex gap-${gap} ${className}`}>
      {children}
    </div>
  );
}

// === COMPOSED LAYOUTS ===

function AppShell({ nav, sidebar, children, footer }) {
  return (
    <Stack gap={0} className="min-h-screen">
      {nav}
      <div className="flex flex-1">
        {sidebar && <aside className="w-64 border-r">{sidebar}</aside>}
        <main className="flex-1 p-6">{children}</main>
      </div>
      {footer}
    </Stack>
  );
}

// Usage
<AppShell
  nav={<TopNavigation />}
  sidebar={<DashboardSidebar />}
  footer={<AppFooter />}
>
  <Stack gap={6}>
    <h1>Dashboard</h1>
    <Cluster gap={4}>
      <StatCard title="Revenue" value="$12,345" />
      <StatCard title="Users" value="1,234" />
      <StatCard title="Orders" value="456" />
    </Cluster>
    <DataTable data={recentOrders} />
  </Stack>
</AppShell>

9. Building a Card System

// === BASE CARD ===
function Card({ children, variant = 'default', className = '', ...props }) {
  const variants = {
    default: 'bg-white border border-gray-200',
    elevated: 'bg-white shadow-lg',
    outlined: 'bg-transparent border-2 border-gray-300',
    ghost: 'bg-gray-50',
  };

  return (
    <div className={`rounded-lg ${variants[variant]} ${className}`} {...props}>
      {children}
    </div>
  );
}

// === CARD SUB-COMPONENTS ===
function CardHeader({ children, action, className = '' }) {
  return (
    <div className={`px-6 py-4 border-b border-gray-100 flex justify-between items-center ${className}`}>
      <div>{children}</div>
      {action && <div>{action}</div>}
    </div>
  );
}

function CardBody({ children, className = '' }) {
  return <div className={`px-6 py-4 ${className}`}>{children}</div>;
}

function CardFooter({ children, className = '' }) {
  return (
    <div className={`px-6 py-4 border-t border-gray-100 ${className}`}>
      {children}
    </div>
  );
}

Card.Header = CardHeader;
Card.Body = CardBody;
Card.Footer = CardFooter;

// === SPECIALIZED CARDS (composition + specialization) ===

function StatCard({ title, value, change, icon }) {
  return (
    <Card>
      <Card.Body>
        <div className="flex justify-between">
          <div>
            <p className="text-sm text-gray-500">{title}</p>
            <p className="text-2xl font-bold">{value}</p>
            {change && (
              <p className={change > 0 ? 'text-green-600' : 'text-red-600'}>
                {change > 0 ? '↑' : '↓'} {Math.abs(change)}%
              </p>
            )}
          </div>
          {icon && <span className="text-3xl">{icon}</span>}
        </div>
      </Card.Body>
    </Card>
  );
}

function ProfileCard({ user, onFollow }) {
  return (
    <Card variant="elevated">
      <Card.Body className="text-center">
        <img src={user.avatar} alt={user.name} className="w-20 h-20 rounded-full mx-auto" />
        <h3 className="mt-2 font-semibold">{user.name}</h3>
        <p className="text-gray-500">{user.bio}</p>
      </Card.Body>
      <Card.Footer className="text-center">
        <button onClick={onFollow} className="btn-primary">Follow</button>
      </Card.Footer>
    </Card>
  );
}

// Usage
<div className="grid grid-cols-3 gap-4">
  <StatCard title="Revenue" value="$12,345" change={12.5} icon="💰" />
  <StatCard title="Users" value="1,234" change={-2.3} icon="👥" />
  
  <Card>
    <Card.Header action={<button></button>}>
      <h3>Recent Activity</h3>
    </Card.Header>
    <Card.Body>
      <ActivityList items={activities} />
    </Card.Body>
    <Card.Footer>
      <a href="/activity">View all</a>
    </Card.Footer>
  </Card>
</div>

10. Building a Modal System

import { createContext, useContext, useState, useEffect } from 'react';
import { createPortal } from 'react-dom';

const ModalContext = createContext();

function Modal({ children, isOpen, onClose }) {
  // Close on Escape
  useEffect(() => {
    if (!isOpen) return;
    const handler = (e) => { if (e.key === 'Escape') onClose(); };
    document.addEventListener('keydown', handler);
    return () => document.removeEventListener('keydown', handler);
  }, [isOpen, onClose]);

  // Prevent body scroll
  useEffect(() => {
    if (isOpen) {
      document.body.style.overflow = 'hidden';
      return () => { document.body.style.overflow = ''; };
    }
  }, [isOpen]);

  if (!isOpen) return null;

  return createPortal(
    <ModalContext.Provider value={{ onClose }}>
      <div className="modal-overlay" onClick={onClose}>
        <div className="modal-content" onClick={e => e.stopPropagation()}>
          {children}
        </div>
      </div>
    </ModalContext.Provider>,
    document.body
  );
}

function ModalHeader({ children }) {
  const { onClose } = useContext(ModalContext);
  return (
    <div className="modal-header">
      <div>{children}</div>
      <button onClick={onClose} className="modal-close">×</button>
    </div>
  );
}

function ModalBody({ children }) {
  return <div className="modal-body">{children}</div>;
}

function ModalFooter({ children }) {
  return <div className="modal-footer">{children}</div>;
}

Modal.Header = ModalHeader;
Modal.Body = ModalBody;
Modal.Footer = ModalFooter;

// === SPECIALIZED MODALS ===

function ConfirmDialog({ isOpen, onClose, onConfirm, title, message, danger = false }) {
  return (
    <Modal isOpen={isOpen} onClose={onClose}>
      <Modal.Header><h2>{title}</h2></Modal.Header>
      <Modal.Body><p>{message}</p></Modal.Body>
      <Modal.Footer>
        <button onClick={onClose} className="btn-secondary">Cancel</button>
        <button
          onClick={() => { onConfirm(); onClose(); }}
          className={danger ? 'btn-danger' : 'btn-primary'}
        >
          Confirm
        </button>
      </Modal.Footer>
    </Modal>
  );
}

// Usage
<ConfirmDialog
  isOpen={showDeleteConfirm}
  onClose={() => setShowDeleteConfirm(false)}
  onConfirm={handleDelete}
  title="Delete User?"
  message="This action cannot be undone."
  danger
/>

11. Composition for Prop Drilling Solutions

Composition directly solves prop drilling by letting parents pass assembled components instead of raw data.

// ❌ Prop drilling: data passes through Layout and Header
function App() {
  const { user } = useAuth();
  return (
    <Layout user={user}>
      <Header user={user} />
      <main><Outlet /></main>
    </Layout>
  );
}

// ✅ Composition: App assembles the components, no drilling needed
function App() {
  const { user } = useAuth();

  return (
    <Layout
      header={
        <Header
          right={<UserMenu user={user} />}  {/* user goes directly to consumer */}
        />
      }
    >
      <main><Outlet /></main>
    </Layout>
  );
}

// Layout and Header never see "user" — they just render slots
function Layout({ header, children }) {
  return (
    <div className="layout">
      {header}
      {children}
    </div>
  );
}

function Header({ left, center, right }) {
  return (
    <header className="flex justify-between items-center p-4">
      <div>{left || <Logo />}</div>
      <div>{center}</div>
      <div>{right}</div>
    </header>
  );
}

12. Composition with TypeScript

import { ReactNode, ComponentPropsWithoutRef } from 'react';

// children prop typing
interface CardProps {
  children: ReactNode;
  variant?: 'default' | 'elevated' | 'outlined';
  className?: string;
}

function Card({ children, variant = 'default', className }: CardProps) {
  return <div className={`card card-${variant} ${className}`}>{children}</div>;
}

// Slot props typing
interface PageLayoutProps {
  header?: ReactNode;
  sidebar?: ReactNode;
  footer?: ReactNode;
  children: ReactNode;
}

function PageLayout({ header, sidebar, footer, children }: PageLayoutProps) {
  return (
    <div className="page-layout">
      {header}
      <div className="flex">
        {sidebar}
        <main>{children}</main>
      </div>
      {footer}
    </div>
  );
}

// Render prop typing
interface FetchDataProps<T> {
  url: string;
  children: (state: { data: T | null; loading: boolean; error: Error | null }) => ReactNode;
}

function FetchData<T>({ url, children }: FetchDataProps<T>) {
  const { data, loading, error } = useFetch<T>(url);
  return <>{children({ data, loading, error })}</>;
}

// Polymorphic component (the "as" prop)
type BoxProps<T extends React.ElementType> = {
  as?: T;
  children?: ReactNode;
} & ComponentPropsWithoutRef<T>;

function Box<T extends React.ElementType = 'div'>({
  as,
  children,
  ...props
}: BoxProps<T>) {
  const Component = as || 'div';
  return <Component {...props}>{children}</Component>;
}

// Usage
<Box>Default div</Box>
<Box as="section">Now a section</Box>
<Box as="a" href="/link">Now an anchor</Box>
<Box as={Link} to="/page">Now a React Router Link</Box>

13. When Composition Goes Wrong

Anti-Pattern: Over-Composition

// ❌ Too many wrappers — "wrapper hell"
<ThemeProvider>
  <AuthProvider>
    <CartProvider>
      <NotificationProvider>
        <LanguageProvider>
          <ToastProvider>
            <ModalProvider>
              <App />
            </ModalProvider>
          </ToastProvider>
        </LanguageProvider>
      </NotificationProvider>
    </CartProvider>
  </AuthProvider>
</ThemeProvider>

// ✅ Solution: compose providers into one
function AppProviders({ children }) {
  return (
    <ThemeProvider>
      <AuthProvider>
        <CartProvider>
          {children}
        </CartProvider>
      </AuthProvider>
    </ThemeProvider>
  );
}

// Or use a utility
function composeProviders(...providers) {
  return ({ children }) =>
    providers.reduceRight(
      (acc, Provider) => <Provider>{acc}</Provider>,
      children
    );
}

const AllProviders = composeProviders(
  ThemeProvider, AuthProvider, CartProvider, NotificationProvider
);

<AllProviders><App /></AllProviders>

Anti-Pattern: Opaque children

// ❌ Component does complex things with children that aren't obvious
function MagicWrapper({ children }) {
  // Clones children, adds event handlers, modifies props,
  // reorders children, adds animations...
  // User has no idea what's happening
  return Children.map(children, (child, i) =>
    cloneElement(child, {
      style: { ...child.props.style, animationDelay: `${i * 100}ms` },
      onClick: (...args) => {
        analytics.track('click', { index: i });
        child.props.onClick?.(...args);
      },
    })
  );
}

14. Real-World Examples from Popular Libraries

Radix UI

<Dialog.Root>
  <Dialog.Trigger>Open</Dialog.Trigger>
  <Dialog.Portal>
    <Dialog.Overlay />
    <Dialog.Content>
      <Dialog.Title>Edit Profile</Dialog.Title>
      <Dialog.Description>Make changes to your profile.</Dialog.Description>
      <Dialog.Close>Close</Dialog.Close>
    </Dialog.Content>
  </Dialog.Portal>
</Dialog.Root>

React Router

<BrowserRouter>
  <Routes>
    <Route path="/" element={<Layout />}>
      <Route index element={<Home />} />
      <Route path="about" element={<About />} />
    </Route>
  </Routes>
</BrowserRouter>

shadcn/ui

<Card>
  <CardHeader>
    <CardTitle>Create project</CardTitle>
    <CardDescription>Deploy in one-click.</CardDescription>
  </CardHeader>
  <CardContent>
    <form>...</form>
  </CardContent>
  <CardFooter>
    <Button>Deploy</Button>
  </CardFooter>
</Card>

All major libraries use compound components and composition.


15. Key Takeaways

  1. Composition over inheritance — React's official recommendation. Combine components instead of extending them.

  2. children is the simplest composition — any JSX between tags becomes the children prop.

  3. Named slots (props that accept ReactNode) provide multiple insertion points in a component.

  4. Specialization creates specific components by pre-filling props on generic ones.

  5. Compound components share implicit state via Context, providing clean declarative APIs (like <Tabs> + <Tab> + <TabPanel>).

  6. Render props let consumers decide the UI while the component provides the logic (mostly replaced by hooks in modern React).

  7. Composition solves prop drilling — pass assembled components as slots instead of passing raw data through intermediaries.

  8. Major libraries all use these patterns — Radix UI, React Router, shadcn/ui, etc.


Explain-It Challenge

  1. Explain to a designer: Using the analogy of a picture frame (frame doesn't know what picture goes inside), explain why React's children prop is powerful.

  2. Design the API: You're building a DataTable component library. How would you use compound components so users can compose <DataTable>, <DataTable.Column>, <DataTable.Toolbar>, and <DataTable.Pagination>?

  3. Refactor with composition: Given an app where <Page user={user} theme={theme}> passes both to <Header user={user} theme={theme}> which passes to <NavBar user={user}> and <ThemeToggle theme={theme}> — show how slot composition eliminates the drilling.


Navigation: ← Prop Drilling Problem · Next → 2.7 Useful Hooks in React