Skip to main content

🧩 Composition vs Inheritance

Object-oriented tutorials teach you to build a tall family tree of classes, each inheriting from the one above. React quietly threw that playbook away. Instead of asking "what does this component extend?", React asks "what does this component contain?" β€” and that single shift makes UIs dramatically easier to reuse, combine, and reason about.

Week 4 · Day 5 (Friday: Component Composition) · Lecture 1

🎯 Learning Objectives

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

  • Explain why React recommends composition over inheritance for reusing UI
  • Use the containment pattern to wrap arbitrary content with children
  • Build specialized components by configuring a generic one through props
  • Pass multiple pieces of UI into named slots via props
  • Share behavior with the render-prop pattern and recognize where higher-order components fit
  • Decide between composition, custom hooks, and (rarely) inheritance for a given problem

Estimated Time: 60 minutes

Practice: Refactor a rigid class hierarchy into a flexible composed Card and Alert system.

In This Lesson

Two Ways to Reuse

Every UI framework has to answer one question: when two components share behavior or appearance, how do you avoid writing it twice? There are two classic answers.

Inheritance is the family-tree answer. A FancyButton extends a Button, borrowing its guts and overriding a piece. Composition is the LEGO answer: you build a big thing by snapping small, independent pieces together. React comes down firmly on the side of composition β€” so firmly that the official docs say they have "yet to find any use cases where we would recommend creating component inheritance hierarchies."

graph TD subgraph Inheritance["Inheritance β€” a rigid tree"] A[Button] --> B[IconButton] A --> C[PrimaryButton] B --> D[PrimaryIconButton??] C --> D end subgraph Composition["Composition β€” snap-together pieces"] E[Button] --> F[uses Icon inside] E --> G[uses Spinner inside] E --> H[uses any children] end

Notice the awkward PrimaryIconButton node on the left: to combine two behaviors you'd need multiple inheritance, which JavaScript doesn't have. On the right there is no combination problem β€” you just place the pieces you want inside. That is the whole argument in one picture, and the rest of this lesson turns it into concrete patterns.

Why Not Inheritance?

Inheritance isn't evil β€” it models genuine "is-a" relationships well. The trouble is that UI is rarely an "is-a" relationship. A dialog isn't a kind of box; it has a box, a title, and some buttons. Forcing that into a class tree creates two recurring headaches.

The fragile base class problem

When many components extend one base, a change to the base ripples into all of them β€” often breaking a subclass that quietly depended on the old behavior. Here is the class-based approach the original lesson warned about:

// ❌ Inheritance approach β€” every variation must override render()
class BaseButton extends React.Component {
    render() {
        return <button>{this.props.label}</button>;
    }
}

class IconButton extends BaseButton {
    render() {
        // Can't just "add an icon" β€” must re-implement the whole thing
        return (
            <button>
                <Icon type={this.props.icon} />
                {this.props.label}
            </button>
        );
    }
}

// Now you want a button that is BOTH primary AND has an icon.
// With inheritance there is no clean way to combine the two.

⚠️ The diamond problem

If a Duck needed to inherit move() from both a FlyingCreature and a SwimmingCreature, which one wins? Languages that allow multiple inheritance wrestle with this "diamond" ambiguity constantly. Composition sidesteps it entirely: a duck simply has a flying ability and a swimming ability as separate pieces.

The composition alternative

Watch how cleanly the same buttons compose. Each component is small, does one thing, and takes children so it never needs to know what goes inside it:

// βœ… Composition β€” small pieces that snap together
function Button({ children, className = '', ...props }) {
    return (
        <button className={`btn ${className}`} {...props}>
            {children}
        </button>
    );
}

function Icon({ type }) {
    return <i className={`icon icon-${type}`} />;
}

// An icon button is just a Button that renders an Icon among its children
function IconButton({ icon, children, ...props }) {
    return (
        <Button {...props}>
            <Icon type={icon} />
            {children}
        </Button>
    );
}

// A primary button is just a Button with a preset className
function PrimaryButton({ children, ...props }) {
    return <Button className="primary" {...props}>{children}</Button>;
}

// "Both" is trivial now β€” compose the two ideas, no new inheritance needed
function PrimaryIconButton({ icon, children, ...props }) {
    return (
        <PrimaryButton {...props}>
            <Icon type={icon} />
            {children}
        </PrimaryButton>
    );
}

Why it matters: the combination that was impossible with inheritance took four lines with composition. Because each piece is independent, you can mix them in any order without a combinatorial explosion of subclasses.

Containment with children

Some components β€” sidebars, dialogs, cards β€” can't know what they'll hold ahead of time. React's answer is the special children prop: whatever you write between a component's tags arrives inside it as children. Think of it as a delivery slot in the component.

A Card component acts as a frame; the content written between its tags is delivered into the children slot <Card> card frame (fixed) { children } the delivery slot <Card>   <h3>Profile</h3>   <p>Ada Lovelace</p> </Card> whatever you nest becomes children
A container component provides the frame; the content you nest between its tags flows into the children slot.
// A Card doesn't care WHAT it wraps β€” only that it wraps something
function Card({ title, children }) {
    return (
        <div className="card">
            <div className="card-header"><h3>{title}</h3></div>
            <div className="card-body">{children}</div>
        </div>
    );
}

// The same Card holds a profile...
function App() {
    return (
        <Card title="User Profile">
            <p>Name: Ada Lovelace</p>
            <p>Email: ada@example.com</p>
        </Card>
    );
}

// ...or a form, or a chart, or anything else β€” no changes to Card.

A modal is the same idea plus a little conditional logic:

function Modal({ isOpen, onClose, children }) {
    if (!isOpen) return null; // render nothing when closed

    return (
        <div className="modal-overlay" onClick={onClose}>
            {/* stopPropagation keeps clicks INSIDE from closing the modal */}
            <div className="modal-content" onClick={(e) => e.stopPropagation()}>
                {children}
            </div>
        </div>
    );
}

βœ… The mental model

A container built on children is like a picture frame: the frame is reusable and fixed, but you can drop any picture into it. You'll go far deeper into children in the very next lesson.

Specialization

Sometimes a component is a more specific case of another β€” a WelcomeDialog is a specific Dialog. In an OOP world you'd reach for a subclass. In React you write a function that renders the generic component with specific props locked in. This is "specialization by configuration."

// Generic, configurable dialog
function Dialog({ title, message, confirmLabel = 'OK', onConfirm, onCancel }) {
    return (
        <div className="dialog">
            <h2>{title}</h2>
            <p>{message}</p>
            <div className="dialog-buttons">
                {onCancel && <button onClick={onCancel}>Cancel</button>}
                <button onClick={onConfirm}>{confirmLabel}</button>
            </div>
        </div>
    );
}

// A specialized dialog = the generic one with specific props baked in
function DeleteConfirmDialog({ itemName, onConfirm, onCancel }) {
    return (
        <Dialog
            title="Confirm deletion"
            message={`Delete "${itemName}"? This cannot be undone.`}
            confirmLabel="Delete"
            onConfirm={onConfirm}
            onCancel={onCancel}
        />
    );
}

function WelcomeDialog({ userName }) {
    return (
        <Dialog
            title="Welcome!"
            message={`Hi ${userName}, glad to have you here.`}
            confirmLabel="Get started"
            onConfirm={() => console.log('started')}
        />
    );
}

Why it matters: the specialized components are tiny, read like documentation, and if the Dialog markup ever changes, every specialization updates for free. That is the maintainability win inheritance promised but composition actually delivers β€” without the fragile-base-class risk.

The Slots Pattern

children is one slot, but a layout often needs several: a header, a sidebar, a main area, a footer. Since props can hold JSX just as easily as strings, you can accept multiple "slots" as named props.

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

// Each slot receives a whole element β€” pass in whatever you like
function Dashboard() {
    return (
        <PageLayout
            header={<TopBar user={currentUser} />}
            sidebar={<NavMenu items={menuItems} />}
            content={<Reports data={reportData} />}
            footer={<SiteFooter />}
        />
    );
}

πŸ’‘ Slots vs a single children

Reach for named slots when a component has distinct regions that each hold different content. Reach for a single children when there's just one open area. Both are "just props holding JSX" under the hood.

Render Props & Higher-Order Components

The patterns so far compose markup. Two more patterns compose behavior β€” sharing stateful logic between components.

Render props

A render prop is a prop whose value is a function that returns JSX. The component owns some state and calls that function, handing the state to the caller so they can decide how to display it.

function MouseTracker({ children }) {
    const [pos, setPos] = React.useState({ x: 0, y: 0 });

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

    // Call children as a function, passing the state down
    return children(pos);
}

// The consumer decides the markup β€” the tracker only supplies data
function App() {
    return (
        <MouseTracker>
            {({ x, y }) => <p>Mouse is at {x}, {y}</p>}
        </MouseTracker>
    );
}

Higher-order components (HOCs)

A higher-order component is a function that takes a component and returns a new, enhanced component. It's a factory that wraps extra behavior around whatever you give it.

graph LR A[Plain Component] --> B["withAuth( )"] B --> C[Enhanced Component] D[extra props / guards] --> B
// A HOC that gates a component behind authentication
function withAuth(WrappedComponent) {
    return function AuthGuarded(props) {
        const { user, loading } = useAuth(); // some custom hook

        if (loading) return <LoadingSpinner />;
        if (!user)   return <Navigate to="/login" />;

        // Pass the original props through, plus the user
        return <WrappedComponent {...props} user={user} />;
    };
}

const ProtectedDashboard = withAuth(Dashboard);

πŸ“– Modern note: prefer custom hooks

HOCs and render props were the pre-2019 way to share logic, and you'll still meet them in libraries and older code β€” so it's worth recognizing them. Today, though, a custom hook (e.g. const { user, loading } = useAuth();) does the same job with less indirection and no "wrapper hell" in your component tree. We cover HOCs properly two lessons from now, then contrast them with hooks. Rule of thumb: reach for a hook first, a HOC only when you truly need to wrap the rendered output.

Choosing an Approach

With five patterns on the table, here's a quick decision guide. In almost every UI case, the answer flows toward composition.

graph TD A[I want to reuse something] --> B{Reusing markup or behavior?} B -->|Markup / layout| C{One open area or several regions?} C -->|One| D[children containment] C -->|Several| E[slots via props] B -->|A preset variant| F[specialization by config] B -->|Stateful behavior| G{Just logic, no wrapper UI?} G -->|Yes| H[custom hook β€” preferred] G -->|Needs to wrap output| I[render prop or HOC]
PatternBest forReach for it when…
Containment (children)Wrappers, cards, modalsThe component holds one open area of arbitrary content
SpecializationPreset variants of a generic componentComponent B is component A with certain props fixed
SlotsLayouts with several regionsYou need header/sidebar/content/footer areas
Render propsSharing state with flexible outputThe consumer must control how shared data renders
HOCWrapping cross-cutting concernsYou must inject props or guard the whole output (legacy-leaning)
Custom hookSharing pure logicAlmost always the modern first choice for shared behavior

And inheritance? Keep it for genuine non-UI "is-a" models β€” a class hierarchy of error types, say. For components, the React team's advice stands: compose.

Practice & Quiz

πŸ‹οΈ Exercise 1: A composable Card system

Goal: Build a Card that accepts a title, an optional footer slot, and a children body β€” so any content can live inside without changing Card.

function Card({ title, footer, children }) {
    // TODO: render a header (only if title exists),
    //       a body containing children,
    //       and a footer region (only if footer exists)
}

// Should work with EITHER of these without edits to Card:
// <Card title="Welcome"><p>Hello!</p></Card>
// <Card title="Stats" footer={<small>Updated now</small>}><Chart /></Card>
πŸ’‘ Hint

Use short-circuit rendering (title && <header>…</header>) so the header and footer only appear when those props were passed. Put {children} in the body.

βœ… Solution
function Card({ title, footer, children }) {
    return (
        <div className="card">
            {title && <div className="card-header"><h3>{title}</h3></div>}
            <div className="card-body">{children}</div>
            {footer && <div className="card-footer">{footer}</div>}
        </div>
    );
}

πŸ‹οΈ Exercise 2: Specialize an Alert

Goal: Given a generic Alert, create SuccessAlert and ErrorAlert specializations by locking in props β€” no copy-pasted markup.

function Alert({ variant, icon, children }) {
    return (
        <div className={`alert alert-${variant}`}>
            <span className="alert-icon">{icon}</span>
            <div>{children}</div>
        </div>
    );
}
// TODO: SuccessAlert and ErrorAlert that reuse Alert
βœ… Solution
function SuccessAlert({ children }) {
    return <Alert variant="success" icon="βœ…">{children}</Alert>;
}

function ErrorAlert({ children }) {
    return <Alert variant="error" icon="β›”">{children}</Alert>;
}

// Usage:
// <SuccessAlert>Saved!</SuccessAlert>
// <ErrorAlert>Something went wrong.</ErrorAlert>

🎯 Quick Quiz

Question 1: React officially recommends which approach for reusing UI between components?

Question 2: What arrives in a component's children prop?

Question 3: For sharing stateful logic in modern React, which is usually the first choice?

Best Practices & Pitfalls

βœ… Do

  • Default to composition; keep components small and single-purpose
  • Use children for one open content area, named slot props for several regions
  • Specialize by wrapping a generic component with preset props, not by copying markup
  • Prefer a custom hook when you only need to share logic, not wrapper UI
  • Always spread the incoming props through to the wrapped component in a HOC

❌ Don't

  • Build inheritance hierarchies of components β€” you'll hit the combination wall fast
  • Reach for a HOC or render prop when a plain custom hook would do
  • Create a HOC-wrapped component inside render β€” it remounts every time (more on this in the HOC lesson)
  • Overload one giant component with a dozen boolean props instead of composing smaller ones

⚠️ "Prop explosion" is a smell

If a component sprouts isPrimary, isIcon, isLarge, isDanger… that's inheritance's combination problem sneaking back in through props. Split it into composable pieces instead.

Summary

πŸŽ‰ Key Takeaways

  • React reuses UI through composition, not inheritance β€” small pieces snapped together
  • Containment uses children so a component can wrap arbitrary content
  • Specialization makes a specific component by configuring a generic one with fixed props
  • Slots pass several regions of UI as named props
  • Render props and HOCs share behavior; a custom hook is the modern first choice

πŸ“š Additional Resources

πŸš€ What's Next?

You've met the children prop briefly here β€” next we go deep. Children Props covers the React.Children utilities, cloning elements to inject props, the children-as-a-function pattern, and how to build compound components like tabs and accordions.

πŸŽ‰ Well composed!

You now think in pieces that snap together instead of trees to extend β€” the mindset behind every good React codebase.