ReactComponent Composition

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:

TSX
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:

TSX
// 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>
  )
}

TSX
// 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>
  )
}
Note
The Card components above know nothing about products or users. They only know about layout and styling. This separation is what makes them truly reusable.
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:

TSX
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>
  )
}
Tip
Named slots are especially useful for layout components like sidebars, split panels, and dashboards where different areas are owned by different parts of the application.
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:

TSX
// 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:

TSX
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 children or named slot props

  • Shared 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

Warning
Never extend your own React components: class UserCard extends Card is an anti-pattern. You lose JSX composition flexibility, make testing harder, and couple unrelated concerns. Always prefer wrapping.
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:

TSX
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>
  )
}
Note
Compound components are covered in depth in the "Compound Components" page. The key idea is that a group of related components communicates through shared context rather than through prop drilling.