π§© 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."
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.
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.
// 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.
| Pattern | Best for | Reach for it when⦠|
|---|---|---|
Containment (children) | Wrappers, cards, modals | The component holds one open area of arbitrary content |
| Specialization | Preset variants of a generic component | Component B is component A with certain props fixed |
| Slots | Layouts with several regions | You need header/sidebar/content/footer areas |
| Render props | Sharing state with flexible output | The consumer must control how shared data renders |
| HOC | Wrapping cross-cutting concerns | You must inject props or guard the whole output (legacy-leaning) |
| Custom hook | Sharing pure logic | Almost 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
childrenfor 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
childrenso 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
- React β Passing props to a component
- React β Passing JSX as children
- React β Reusing logic with custom hooks
π 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.