Component Composition
Composition is the practice of building complex UIs by combining simple, focused components together. It is the design pattern at the heart of React. Instead of creating one large component that does everything, you build a library of small components and compose them — much like assembling LEGO bricks rather than carving a shape out of one solid block.
React deliberately favors composition over inheritance. You will almost never extend a React component class to add behavior — instead, you wrap, pass, and combine.
The children Prop
The most fundamental composition tool is the children prop. When you place JSX between the opening and closing tags of a component, React passes it to that component as props.children:
function Panel({ children }: { children: React.ReactNode }) {
return (
<div className="panel">
{children}
</div>
)
}
// The JSX between the tags becomes children
function App() {
return (
<Panel>
<h2>Hello from inside the Panel</h2>
<p>I am a child of Panel.</p>
</Panel>
)
}Panel does not know what its children will be. It is a generic container that lends its styling and layout to whatever is passed in. This is the open/closed principle applied to UI: Panel is open for extension (new content can be passed in) but closed for modification (you never change Panel to accommodate a new use case).
Building a Card Component Hierarchy
A common real-world pattern is a Card broken into CardHeader, CardBody, and CardFooter. Each piece is simple on its own; together they form a flexible, composable layout unit:
// Each sub-component is tiny and focused
function CardHeader({ children }: { children: React.ReactNode }) {
return (
<div className="card-header">
{children}
</div>
)
}
function CardBody({ children }: { children: React.ReactNode }) {
return (
<div className="card-body">
{children}
</div>
)
}
function CardFooter({ children }: { children: React.ReactNode }) {
return (
<div className="card-footer">
{children}
</div>
)
}
function Card({ children }: { children: React.ReactNode }) {
return (
<div className="card">
{children}
</div>
)
}// Composed to build a product listing card
function ProductCard({ product }: { product: Product }) {
return (
<Card>
<CardHeader>
<img src={product.imageUrl} alt={product.name} />
</CardHeader>
<CardBody>
<h3>{product.name}</h3>
<p>{product.description}</p>
</CardBody>
<CardFooter>
<span className="price">${product.price}</span>
<button>Add to cart</button>
</CardFooter>
</Card>
)
}
// The same Card shell used for a user profile
function ProfileCard({ user }: { user: User }) {
return (
<Card>
<CardHeader>
<Avatar src={user.avatarUrl} />
</CardHeader>
<CardBody>
<h3>{user.name}</h3>
<p>{user.bio}</p>
</CardBody>
</Card>
)
}Named Slots (Multiple Content Areas)
The children prop handles one content area. For components with multiple distinct areas — a dialog with a title, body, and action buttons — use explicit named props as slots:
interface DialogProps {
title: React.ReactNode
actions: React.ReactNode
children: React.ReactNode // the main body content
}
function Dialog({ title, actions, children }: DialogProps) {
return (
<div role="dialog" aria-modal="true" className="dialog">
<div className="dialog-title">{title}</div>
<div className="dialog-body">{children}</div>
<div className="dialog-actions">{actions}</div>
</div>
)
}
// Usage — each slot receives any JSX
function ConfirmDeleteDialog({ onConfirm, onCancel }: ConfirmProps) {
return (
<Dialog
title={<h2>Delete Item</h2>}
actions={
<>
<button onClick={onCancel}>Cancel</button>
<button onClick={onConfirm} className="danger">Delete</button>
</>
}
>
<p>Are you sure you want to delete this item? This action cannot be undone.</p>
</Dialog>
)
}Specialization (Wrapping a Generic Component)
Specialization means creating a focused component by wrapping a generic one with specific props pre-filled. The specialized component is a thin wrapper — it does not duplicate logic, it just configures the generic component for a particular use case:
// Generic button component
interface ButtonProps {
variant?: 'primary' | 'secondary' | 'danger' | 'ghost'
size?: 'sm' | 'md' | 'lg'
disabled?: boolean
onClick?: () => void
children: React.ReactNode
}
function Button({ variant = 'primary', size = 'md', ...rest }: ButtonProps) {
return (
<button className={`btn btn--${variant} btn--${size}`} {...rest} />
)
}
// Specialized variants — each is just a configured Button
function DangerButton(props: Omit<ButtonProps, 'variant'>) {
return <Button variant="danger" {...props} />
}
function GhostButton(props: Omit<ButtonProps, 'variant'>) {
return <Button variant="ghost" {...props} />
}
// Usage
<DangerButton onClick={handleDelete}>Delete Account</DangerButton>
<GhostButton onClick={handleCancel}>Cancel</GhostButton>Passing Components as Props
Because JSX is just JavaScript, you can pass a component (or a piece of JSX) as a prop to another component. This gives the parent full control over what gets rendered in a specific slot — a powerful technique for building truly flexible layouts:
interface ListProps<T> {
items: T[]
renderItem: (item: T, index: number) => React.ReactNode
emptyState?: React.ReactNode
}
function List<T>({ items, renderItem, emptyState }: ListProps<T>) {
if (items.length === 0) {
return <>{emptyState ?? <p>No items to display.</p>}</>
}
return (
<ul>
{items.map((item, index) => (
<li key={index}>{renderItem(item, index)}</li>
))}
</ul>
)
}
// The parent decides how each item looks
function UserList({ users }: { users: User[] }) {
return (
<List
items={users}
renderItem={user => (
<div className="user-row">
<Avatar src={user.avatarUrl} />
<span>{user.name}</span>
</div>
)}
emptyState={<p>No users found. <a href="/invite">Invite someone</a>.</p>}
/>
)
}Composition vs Inheritance
In object-oriented languages, it is common to extend a base class to share behavior. The React team explicitly recommends not using inheritance for UI components. Composition covers every use case more flexibly:
Shared UI / layout → use
childrenor named slot propsShared behavior / logic → extract to a custom hook
Specialization → wrap the generic component with specific props
Cross-cutting concerns → use a Higher-Order Component or context
Compound Components Pattern
A more advanced composition technique is the compound component pattern, where a parent component and several child components share implicit state via context. This is how React libraries like Radix UI and Headless UI are built:
import { createContext, useContext, useState } from 'react'
// Shared state lives in context
const TabsContext = createContext<{
activeTab: string
setActiveTab: (tab: string) => void
} | null>(null)
function Tabs({ children, defaultTab }: { children: React.ReactNode; defaultTab: string }) {
const [activeTab, setActiveTab] = useState(defaultTab)
return (
<TabsContext.Provider value={{ activeTab, setActiveTab }}>
<div className="tabs">{children}</div>
</TabsContext.Provider>
)
}
function TabList({ children }: { children: React.ReactNode }) {
return <div role="tablist" className="tab-list">{children}</div>
}
function Tab({ value, children }: { value: string; children: React.ReactNode }) {
const ctx = useContext(TabsContext)!
return (
<button
role="tab"
aria-selected={ctx.activeTab === value}
onClick={() => ctx.setActiveTab(value)}
>
{children}
</button>
)
}
function TabPanel({ value, children }: { value: string; children: React.ReactNode }) {
const ctx = useContext(TabsContext)!
if (ctx.activeTab !== value) return null
return <div role="tabpanel">{children}</div>
}
// Composed — the API reads like HTML
function App() {
return (
<Tabs defaultTab="overview">
<TabList>
<Tab value="overview">Overview</Tab>
<Tab value="details">Details</Tab>
<Tab value="reviews">Reviews</Tab>
</TabList>
<TabPanel value="overview"><p>Overview content</p></TabPanel>
<TabPanel value="details"><p>Details content</p></TabPanel>
<TabPanel value="reviews"><p>Reviews content</p></TabPanel>
</Tabs>
)
}