Skip to main content

🏗️ Component Composition Patterns

Once you can write components and pass them props, the next skill is arranging them well. Composition — building complex UIs by combining simple components — is React's core philosophy. This lesson tours the patterns that make components flexible and reusable, from the humble children prop to compound components and custom Hooks.

🎯 Learning Objectives

By the end of this lesson, you will be able to:

  • Use the children prop for containment and build multi-slot layouts
  • Apply the specialization pattern to create component variants without inheritance
  • Build a compound component system that shares state via Context
  • Recognize the render props pattern and refactor it into a custom Hook
  • Explain why React favors composition over inheritance

Estimated Time: 40–50 minutes  •  Difficulty: Intermediate

Hands-on: Refactor a prop-heavy Card into a flexible compound component.

In This Lesson

The Power of Composition

Composition means building bigger things out of smaller, independent pieces. In React, you compose complex screens from simple components rather than creating elaborate single components or deep inheritance hierarchies. It's the pattern the whole library is designed around.

💡 Analogy — LEGO vs. carving a statue. Composition is like building with LEGO: independent bricks you connect and rearrange freely. Inheritance is like carving a statue from one block — once shaped, it's hard to repurpose a piece. React deliberately chooses the LEGO approach because it stays flexible as requirements change.
graph TD A[Page] --> B[Layout] B --> C[Header] B --> D[Sidebar] B --> E[Content] E --> F[Card] E --> G[Card] F --> H[Button] G --> I[Button]

The same small Card and Button components appear again and again. Composition is what lets a handful of well-designed pieces assemble into an entire application.

Containment with children

Some components — a card, a modal, a panel — don't know their contents ahead of time; they just provide a frame. These are generic containers, and the children prop is how they receive whatever you nest inside them.

function Card({ title, children }) {
  return (
    <div className="card">
      {title && (
        <div className="card-header">
          <h2>{title}</h2>
        </div>
      )}
      <div className="card-body">{children}</div>
    </div>
  );
}

// Each use fills the same frame with different content
function App() {
  return (
    <>
      <Card title="User Profile">
        <p>Name: John Doe</p>
        <button>Edit Profile</button>
      </Card>

      <Card title="Recent Activity">
        <ul>
          <li>Logged in 2 hours ago</li>
          <li>Updated profile picture</li>
        </ul>
      </Card>
    </>
  );
}

✅ Why this beats configuration props

You could instead give Card a dozen props (bodyText, listItems, buttonLabel…) and reassemble them inside. But children lets the caller pass any JSX, so one small component handles infinite layouts without growing a giant prop list. Fewer props, more flexibility.

Multiple Named Slots

children is a single slot. When a layout needs several distinct regions, pass JSX through named props — each one is its own slot:

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

function App() {
  return (
    <PageLayout
      header={<h1>My Application</h1>}
      sidebar={<nav>{/* links */}</nav>}
      main={<Dashboard />}
      footer={<p>&copy; 2026</p>}
    />
  );
}
A layout component with four named slots A PageLayout container holds a header slot on top, a sidebar and main slot in the middle, and a footer slot at the bottom, each filled by a named prop. PageLayout header prop sidebar prop main prop footer prop
Figure 1 — Named props act as multiple content slots, letting one layout component arrange four independent regions.

Specialization

Sometimes one component is a specific case of a more general one — a confirmation dialog is a specialized dialog; a primary button is a specialized button. In object-oriented code you might use inheritance. In React, you specialize through composition: the specific component renders the general one and fills in the specifics.

// General component
function Dialog({ title, children }) {
  return (
    <div className="dialog">
      <h2>{title}</h2>
      <div className="dialog-body">{children}</div>
    </div>
  );
}

// Specialized version built by composing Dialog
function ConfirmDialog({ onConfirm, onCancel, ...rest }) {
  return (
    <Dialog {...rest}>
      <div className="dialog-actions">
        <button onClick={onCancel}>Cancel</button>
        <button className="primary" onClick={onConfirm}>Confirm</button>
      </div>
    </Dialog>
  );
}

The same idea powers a component library's button variants — each is a thin wrapper that forwards props to a base Button:

function Button({ className = '', children, ...rest }) {
  return (
    <button className={`btn ${className}`.trim()} {...rest}>
      {children}
    </button>
  );
}

const PrimaryButton = (props) => <Button className="btn-primary" {...props} />;
const DangerButton  = (props) => <Button className="btn-danger" {...props} />;

Compound Components

Compound components are a set of components designed to work together and share implicit state — think <select> and <option> in HTML, or the tabs on a settings page. The parent manages the shared state; the children read it through Context so the consumer's markup stays clean and declarative.

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

const TabsContext = createContext(null);

function Tabs({ children, defaultIndex = 0 }) {
  const [activeIndex, setActiveIndex] = useState(defaultIndex);
  return (
    <TabsContext.Provider value={{ activeIndex, setActiveIndex }}>
      <div className="tabs">{children}</div>
    </TabsContext.Provider>
  );
}

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

function Tab({ index, children }) {
  const { activeIndex, setActiveIndex } = useContext(TabsContext);
  const isActive = activeIndex === index;
  return (
    <button
      role="tab"
      aria-selected={isActive}
      className={isActive ? 'tab active' : 'tab'}
      onClick={() => setActiveIndex(index)}
    >
      {children}
    </button>
  );
}

function TabPanels({ children }) {
  const { activeIndex } = useContext(TabsContext);
  return <div className="tab-panels">{children[activeIndex]}</div>;
}

// Attach the parts to the parent for a tidy namespace
Tabs.List = TabList;
Tabs.Tab = Tab;
Tabs.Panels = TabPanels;

The payoff is the consumer's code — no state wiring, just a clear structure:

function ProductInfo() {
  return (
    <Tabs>
      <Tabs.List>
        <Tabs.Tab index={0}>Description</Tabs.Tab>
        <Tabs.Tab index={1}>Specs</Tabs.Tab>
        <Tabs.Tab index={2}>Reviews</Tabs.Tab>
      </Tabs.List>
      <Tabs.Panels>
        <p>A great product.</p>
        <ul><li>Weight: 2 lbs</li></ul>
        <p>No reviews yet.</p>
      </Tabs.Panels>
    </Tabs>
  );
}

💡 You've seen this pattern

Popular libraries lean on it heavily: React Bootstrap's Dropdown.Toggle / Dropdown.Menu, Reach UI, Radix, and Headless UI all expose compound components. Recognizing the pattern makes those libraries instantly readable.

Render Props → Custom Hooks

A render prop is a prop whose value is a function that returns JSX. The component owns some logic (mouse position, data fetching) and hands the result to a function you supply, letting you decide how to render it.

function MouseTracker({ render }) {
  const [pos, setPos] = useState({ x: 0, y: 0 });
  return (
    <div onMouseMove={(e) => setPos({ x: e.clientX, y: e.clientY })}>
      {render(pos)}
    </div>
  );
}

// The caller controls the output
<MouseTracker render={(pos) => <p>X: {pos.x}, Y: {pos.y}</p>} />

Render props were the pre-Hooks way to share stateful logic between components. Since Hooks arrived, most of these cases are cleaner as a custom Hook — a plain function that starts with use and calls other Hooks:

import { useState, useEffect } from 'react';

// Reusable logic, extracted into a custom Hook
function useMousePosition() {
  const [pos, setPos] = useState({ x: 0, y: 0 });

  useEffect(() => {
    const handle = (e) => setPos({ x: e.clientX, y: e.clientY });
    window.addEventListener('mousemove', handle);
    return () => window.removeEventListener('mousemove', handle);
  }, []);

  return pos;
}

// Any component can now reuse it with zero nesting
function Readout() {
  const { x, y } = useMousePosition();
  return <p>X: {x}, Y: {y}</p>;
}

✅ The modern default

For sharing logic, reach for a custom Hook first: no wrapper components, no "wrapper hell," and better composition. Render props and higher-order components (functions that wrap a component to add behavior) still appear in libraries and legacy code, so it's worth recognizing them — but you'll rarely reach for them in new code.

Hands-on: A Flexible Card

🏋️ Refactor from props to composition

Objective: This Card has grown a long, rigid prop list. Refactor it into a compound component so callers can assemble exactly the parts they need.

// Before — a rigid, prop-heavy Card
function Card({ title, subtitle, imageSrc, content, actionLabel, onAction }) {
  return (
    <div className="card">
      {imageSrc && <img src={imageSrc} alt="" />}
      <h3>{title}</h3>
      {subtitle && <p className="subtitle">{subtitle}</p>}
      <div className="content">{content}</div>
      {actionLabel && <button onClick={onAction}>{actionLabel}</button>}
    </div>
  );
}
💡 Hint

Make Card a simple container that renders children. Then attach sub-components — Card.Image, Card.Title, Card.Body, Card.Actions — as properties of the Card function. Each sub-component is a tiny function that wraps its children in the right element. No Context is needed here because the parts don't share state.

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

Card.Image = function CardImage({ src, alt = '' }) {
  return <img className="card-image" src={src} alt={alt} />;
};

Card.Title = function CardTitle({ children }) {
  return <h3 className="card-title">{children}</h3>;
};

Card.Body = function CardBody({ children }) {
  return <div className="card-body">{children}</div>;
};

Card.Actions = function CardActions({ children }) {
  return <div className="card-actions">{children}</div>;
};

// Usage — pick exactly the parts you need, in any order
function ProductCard({ product }) {
  return (
    <Card variant="product">
      <Card.Image src={product.image} alt={product.name} />
      <Card.Title>{product.name}</Card.Title>
      <Card.Body>
        <p>{product.description}</p>
      </Card.Body>
      <Card.Actions>
        <button>Add to Cart</button>
        <button>Details</button>
      </Card.Actions>
    </Card>
  );
}

The rigid prop list is gone. A blog post, a user profile, and a product all reuse the same Card parts, arranged however each screen needs.

Best Practices

✅ Do

  • Prefer composition over configuration — reach for children and sub-components before adding another boolean prop.
  • Keep components single-purpose — small pieces compose better than one component that tries to do everything.
  • Reach for custom Hooks to share stateful logic in new code.
  • Use Context inside compound components to share state without prop drilling between the parts.

⚠️ Don't

  • Don't use class inheritance to share UI code — React explicitly recommends composition instead.
  • Don't let a component's prop list balloon into dozens of configuration flags; that's a signal to compose.
  • Don't spread {...props} blindly onto DOM elements — you can leak unexpected or invalid attributes.
  • Don't stack many higher-order components; the nesting ("wrapper hell") is exactly what Hooks were designed to avoid.

Summary & Quiz

🎉 Key Takeaways

  • Composition — building UIs from small pieces — is React's preferred model over inheritance.
  • The children prop enables generic containers; named props give you multiple slots.
  • Specialization creates component variants by rendering a more general component.
  • Compound components share state via Context to offer a clean, declarative API (like Tabs.Tab).
  • Custom Hooks are the modern way to share stateful logic, replacing most render-prop and HOC use.

🎯 Quick Quiz

Question 1: What is the children prop used for?

Question 2: How do the parts of a compound component (like Tabs and Tab) typically share state?

Question 3: In modern React, what's usually the best way to share stateful logic between components?

📚 Further Reading

🚀 What's Next?

You can now structure and combine components with confidence. Next, in Component State Fundamentals, we go deeper on state itself — what it is, when to use it, and how React re-renders in response to it.

🎉 Excellent!

Composition is the skill that separates tidy React codebases from tangled ones.