Skip to main content

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

tsx
// 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 children prop.
  • 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

AspectCompositionInheritance
FlexibilityMix any components freelyFixed parent-child chain
ReusabilityWrap unrelated UIsOnly extends direct children
MaintenanceChange wrapper independentlyBase changes ripple everywhere
React supportOfficial pattern, hooks-friendlyDiscouraged past React.Component
When to use99% of casesLegacy class code only

Common mistakes

Mistake: subclassing your own components

tsx
// 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

tsx
// 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

tsx
// 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 extending Modal.
  • 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 via children.

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

tsx
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

tsx
// 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 ready
Premium

A concise answer to help you respond confidently on this topic during an interview.

Finished reading?