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
- What Is Composition?
- Composition vs Inheritance
- The children Prop
- Slot Pattern (Named Slots)
- Specialization Pattern
- Compound Components Pattern
- Render Props Pattern
- Building a Layout System
- Building a Card System
- Building a Modal System
- Composition for Prop Drilling Solutions
- Composition with TypeScript
- When Composition Goes Wrong
- Real-World Examples from Popular Libraries
- 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:
- Fragile base class — changing Button breaks IconButton and DangerIconButton
- Diamond problem — what if you need IconButton AND ToggleButton?
- Rigid hierarchy — can't easily rearrange
- 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
| Aspect | Inheritance | Composition |
|---|---|---|
| Flexibility | Low — fixed hierarchy | High — mix and match |
| Coupling | Tight — child knows parent | Loose — connected via props |
| Reuse | Extend or override | Wrap or combine |
| Refactoring | Dangerous — breaks children | Safe — 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
-
Composition over inheritance — React's official recommendation. Combine components instead of extending them.
-
childrenis the simplest composition — any JSX between tags becomes thechildrenprop. -
Named slots (props that accept
ReactNode) provide multiple insertion points in a component. -
Specialization creates specific components by pre-filling props on generic ones.
-
Compound components share implicit state via Context, providing clean declarative APIs (like
<Tabs>+<Tab>+<TabPanel>). -
Render props let consumers decide the UI while the component provides the logic (mostly replaced by hooks in modern React).
-
Composition solves prop drilling — pass assembled components as slots instead of passing raw data through intermediaries.
-
Major libraries all use these patterns — Radix UI, React Router, shadcn/ui, etc.
Explain-It Challenge
-
Explain to a designer: Using the analogy of a picture frame (frame doesn't know what picture goes inside), explain why React's
childrenprop is powerful. -
Design the API: You're building a
DataTablecomponent library. How would you use compound components so users can compose<DataTable>,<DataTable.Column>,<DataTable.Toolbar>, and<DataTable.Pagination>? -
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