Composition vs inheritance in React
Composition vs inheritance in React - composition assembles UI from nested components via props like children, while inheritance extends class hierarchies through extends.
Theory
TL;DR
- Inheritance is like Russian dolls: one fits inside another in a fixed order. Composition is like LEGO: snap any pieces together freely.
- Main difference: composition reuses components without locking you into parent-child class chains.
- React docs explicitly say: never subclass your own components beyond
React.Component. - Decision rule: use composition in 99% of cases. Hooks replace the remaining 1% where shared logic once pushed teams toward inheritance.
Quick example
// Inheritance (avoid): rigid and brittle
class Popup extends React.Component {
render() { return <div className="popup">{this.props.children}</div>; }
}
class Dialog extends Popup { // locked to Popup's structure
// any change in Popup breaks Dialog
}
// Composition (preferred): flexible
function Popup({ children }: { children: React.ReactNode }) {
return <div className="popup">{children}</div>;
}
function Dialog({ title, children }: { title: string; children: React.ReactNode }) {
return (
<Popup>
<h2>{title}</h2>
{children}
</Popup>
);
}Dialog wraps Popup without extending it. You can swap Popup for anything without touching Dialog.
Key difference
Class inheritance creates a single prototype chain. Change the base class and everything downstream breaks. Composition passes data through props and keeps components independent. React's fiber architecture traverses the JSX tree top-down, mounting each composed child as a separate unit with its own state and lifecycle. Inheritance flattens that into one object. That is why composition works cleanly with hooks, and why the React team stopped recommending class hierarchies after hooks landed in 16.8.
When to use
- Generic wrapper around any content (modal, card, layout) - use the
childrenprop. - Multiple behaviors on one component (loading state plus tooltip on a button) - compose with HOCs or hooks.
- Shared state across the app (auth, theme, locale) - Context API, not inheritance.
- Simple visual reuse (card, badge, layout shell) - composition, always.
Comparison table
| Aspect | Composition | Inheritance |
|---|---|---|
| Flexibility | Mix any components freely | Fixed parent-child chain |
| Reusability | Wrap unrelated UIs | Only extends direct children |
| Maintenance | Change wrapper independently | Base changes ripple everywhere |
| React support | Official pattern, hooks-friendly | Discouraged past React.Component |
| When to use | 99% of cases | Legacy class code only |
Common mistakes
Mistake: subclassing your own components
// Wrong: fragile chain
class ThemedButton extends React.Component { /* theme prop */ }
class PrimaryButton extends ThemedButton { /* breaks if theme changes */ }
// Fix: variant prop + Context for theme
function Button({ variant, children }: { variant: string; children: React.ReactNode }) {
return <button className={`btn-${variant}`}>{children}</button>;
}The chain ThemedButton -> PrimaryButton -> DangerButton looks tidy until you need to change the base. Then every subclass breaks.
Mistake: expecting children to receive data automatically
// Wrong: UserInfo gets no data
<Card><UserInfo /></Card>
// Fix: render props when children need parent data
<Card renderUser={(user) => <UserInfo user={user} />} />Children do not inherit parent state. You pass it explicitly or pull it from Context.
Mistake: calling hooks inside class components
// Wrong: invalid hook call
class MyComponent extends React.Component {
render() {
const [count, setCount] = useState(0); // Error
}
}Hooks only work inside functional components. For shared stateful logic, extract a custom hook and use it across components.
Real-world usage
- Material-UI v5:
<Modal><Form /></Modal>wraps any content without extendingModal. - Next.js 14 app router:
<Layout><Page /></Layout>is the standard layout composition pattern. - Chakra UI: compound components like
<Box>accept any children. - React Aria:
<Dialog>takes any content viachildren.
Follow-up questions
Q: Why does React have React.Component at all if inheritance is discouraged?
A: React.Component is a thin base class for lifecycle integration. It is the one sanctioned use of inheritance in React. User-defined subclassing beyond that violates single responsibility, which is what the docs warn against.
Q: Show how to share logic between components without inheritance.
A: Extract a custom hook. useModal() returns state and handlers. Any component can call it. No extends needed.
Q: When would you pick Context over composition?
A: Composition is explicit: you pass props directly. Context fits data that many components at different tree levels need without prop drilling (theme, auth, locale). Use composition first; reach for Context when prop chains get too deep.
Q: What replaced mixins, and why were mixins worse than inheritance?
A: Hooks replaced mixins. Mixins patched methods directly onto a component's prototype, which caused naming collisions and made code impossible to trace. Hooks compose pure functions with no side effects on the component object. That is the senior-level answer interviewers look for.
Examples
Basic: card with any content
function Card({ title, children }: { title: string; children: React.ReactNode }) {
return (
<div className="card">
<h3>{title}</h3>
<div className="card-body">{children}</div>
</div>
);
}
// Same Card, two completely different interiors
<Card title="Profile">
<Avatar src={user.avatar} />
<UserInfo name={user.name} />
</Card>
<Card title="Settings">
<SettingsForm />
</Card>One Card component. Two completely different interiors. No extends.
Intermediate: slot pattern for a page layout
// Named slots via props - used by Next.js app router layouts
function Layout({
header,
sidebar,
content,
}: {
header: React.ReactNode;
sidebar: React.ReactNode;
content: React.ReactNode;
}) {
return (
<div className="layout">
<header>{header}</header>
<aside>{sidebar}</aside>
<main>{content}</main>
</div>
);
}
// Each slot gets its own component
<Layout
header={<Navbar user={currentUser} />}
sidebar={<Sidebar links={navLinks} />}
content={<DashboardPage />}
/>Layout has no idea what Navbar, Sidebar, or DashboardPage look like. It just arranges them. I have used this pattern in every non-trivial React project and it holds up well when the design changes, because you can swap any slot without touching the others.
Short Answer
Interview readyA concise answer to help you respond confidently on this topic during an interview.