How does the composition pattern work in React?
The Short Answer
The composition pattern in React means building complex UIs by combining smaller, focused components together rather than relying on inheritance. Instead of extending a base component, you pass components as children or props to create flexible, reusable structures.
React explicitly recommends composition over inheritance. In practice, you almost never need inheritance in React — composition gives you the same code reuse benefits with far more flexibility.
Why Composition Over Inheritance
In object-oriented programming, inheritance creates rigid hierarchies. A Button extends BaseComponent, a PrimaryButton extends Button, a PrimaryIconButton extends PrimaryButton... and soon you have a fragile tree where changing the base breaks everything below it.
Composition flips this around. Instead of building specialized components by extending generic ones, you build them by combining generic ones together. Components don't need to know what their children will be ahead of time.
Pattern 1: Children Prop (Containment)
The simplest and most common form of composition: a component doesn't know its children ahead of time. It renders whatever is passed via the children prop. This is perfect for generic containers like cards, modals, layouts, and wrappers.
// Generic card container — doesn't know or care what goes inside
function Card({ children }: { children: React.ReactNode }) {
return (
<div className="rounded-lg border bg-card p-6 shadow-sm">
{children}
</div>
);
}
// Usage — compose any content inside the card
function UserProfile() {
return (
<Card>
<h2>John Doe</h2>
<p>Software Engineer at Acme Corp</p>
<button>Follow</button>
</Card>
);
}
function ProductCard({ product }: { product: Product }) {
return (
<Card>
<img src={product.image} alt={product.name} />
<h3>{product.name}</h3>
<span>${product.price}</span>
</Card>
);
}
The Card component is infinitely reusable because it makes no assumptions about what goes inside it. You can put a user profile, a product listing, a form, or anything else — the Card just provides the visual container.
Pattern 2: Named Slots
Sometimes a component needs multiple "holes" to fill — not just one children area. You pass multiple components as named props. This gives you explicit control over where each piece renders.
type PageLayoutProps = {
header: React.ReactNode;
sidebar: React.ReactNode;
children: React.ReactNode;
};
function PageLayout({ header, sidebar, children }: PageLayoutProps) {
return (
<div className="min-h-screen grid grid-rows-[auto_1fr] grid-cols-[250px_1fr]">
<header className="col-span-full border-b">{header}</header>
<aside className="border-r p-4">{sidebar}</aside>
<main className="p-6">{children}</main>
</div>
);
}
// Usage — each slot is independently composable
function DashboardPage() {
return (
<PageLayout
header={<Navbar user={currentUser} />}
sidebar={<DashboardNav activeItem="analytics" />}
>
<AnalyticsDashboard />
</PageLayout>
);
}
Pattern 3: Specialization
Sometimes you want a more specific version of a generic component. Instead of inheritance (class ErrorDialog extends Dialog), you create a component that renders the generic one with certain props pre-configured:
// Generic dialog — handles all the rendering logic
function Dialog({ title, message, icon, variant, actions }: DialogProps) {
return (
<div className={`dialog dialog-${variant}`}>
<div className="dialog-icon">{icon}</div>
<h2>{title}</h2>
<p>{message}</p>
{actions && <div className="dialog-actions">{actions}</div>}
</div>
);
}
// Specialized versions — no inheritance, just composition
function SuccessDialog({ message }: { message: string }) {
return (
<Dialog
title="Success!"
message={message}
icon={<CheckIcon />}
variant="success"
/>
);
}
function ErrorDialog({ message }: { message: string }) {
return (
<Dialog
title="Something went wrong"
message={message}
icon={<AlertIcon />}
variant="error"
/>
);
}
Each specialized dialog is just a thin wrapper that pre-configures the generic Dialog. If you need a new type of dialog, you create another wrapper — no need to modify the base component.
Pattern 4: Render Props
A render prop is a function passed as a prop (often as children) that a component calls to determine what to render. In the example below, MouseTracker owns the mouse-position logic and state, but it doesn't decide how to display that data — instead, it calls the function it received as children and passes the current position. The consumer component decides the rendering.
// Component that manages mouse position logic
function MouseTracker({ children }: {
children: (pos: { x: number; y: number }) => React.ReactNode
}) {
const [position, setPosition] = useState({ x: 0, y: 0 });
useEffect(() => {
const handleMove = (e: MouseEvent) => {
setPosition({ x: e.clientX, y: e.clientY });
};
window.addEventListener('mousemove', handleMove);
return () => window.removeEventListener('mousemove', handleMove);
}, []);
return <>{children(position)}</>;
}
// Consumer decides how to render the data
function App() {
return (
<MouseTracker>
{({ x, y }) => <p>Mouse is at ({x}, {y})</p>}
</MouseTracker>
);
}
Modern alternative: Custom hooks
Custom hooks have largely replaced render props for sharing stateful logic. A useMousePosition() hook is simpler. But render props are still useful when you need to compose JSX structure, not just logic.
Choosing the Right Pattern
| Pattern | Use when | Example |
|---|---|---|
| Children (containment) | Generic wrapper that can hold anything | Card, Modal, Layout |
| Named slots | Component has multiple distinct areas | PageLayout, SplitPane |
| Specialization | Pre-configured versions of a generic component | SuccessDialog, PrimaryButton |
| Render props | Parent owns logic, child decides rendering | MouseTracker, DataFetcher |
Why Interviewers Ask This
This question tests whether you understand React's fundamental design philosophy. Interviewers want to see that you can build flexible, reusable component architectures without reaching for inheritance. Knowing multiple composition patterns and when to use each shows you can design component APIs that are both powerful and easy to use.
Quick Revision Cheat Sheet
Core principle: Build complex UIs by combining simple components, not by extending base classes
Containment: Use children prop for generic wrappers (cards, modals, layouts)
Named slots: Multiple props for components with several distinct areas to fill
Specialization: Wrap a generic component with pre-set props instead of extending it
Rule of thumb: If you're thinking about inheritance in React, use composition instead