If you've learned object-oriented programming in languages like Java or C++, you've been taught that inheritance is the way to reuse code. Create a base class, extend it, override methods — it's the OOP way.
But React's official documentation says something surprising: "At Facebook, we use React in thousands of components, and we haven't found any use cases where we would recommend creating component inheritance hierarchies."
Why? Because composition is more flexible, more explicit, and easier to maintain than inheritance.
The Golden Rule
In JavaScript and React, prefer composition ("has-a" relationships) over inheritance ("is-a" relationships). Build complex components by combining simpler ones, not by creating deep class hierarchies.
In simpler terms: Instead of saying "Button is-a Component," say "Button has-a label and has-a onClick handler."
Let's understand why this matters and how to apply it.
Part 1: Inheritance vs Composition in JavaScript
Inheritance: The "Is-A" Relationship
Inheritance models an "is-a" relationship using class hierarchies.
class Animal {
constructor(name) {
this.name = name;
}
eat() {
console.log(`${this.name} is eating`);
}
sleep() {
console.log(`${this.name} is sleeping`);
}
}
class Dog extends Animal {
bark() {
console.log(`${this.name} barks`);
}
}
class Cat extends Animal {
meow() {
console.log(`${this.name} meows`);
}
}
const dog = new Dog('Buddy');
dog.eat(); // "Buddy is eating"
dog.bark(); // "Buddy barks"
The relationship: Dog is-a Animal, Cat is-a Animal.
Looks good so far, right? Now let's add more behavior...
The Problem with Deep Inheritance
What if we want:
- A
Robotthat canwalk()but doesn't needeat()orsleep() - A
FlyingDogthat canbark()andfly() - A
SwimmingRobotthat canwalk()andswim()
Inheritance solution (becomes messy):
class Animal {
eat() {}
sleep() {}
}
class WalkingAnimal extends Animal {
walk() {}
}
class FlyingAnimal extends Animal {
fly() {}
}
// Problem: Dog needs both walking AND barking
class Dog extends WalkingAnimal {
bark() {}
}
// Problem: FlyingDog can't extend both Dog and FlyingAnimal!
// JavaScript doesn't support multiple inheritance
class FlyingDog extends ??? {
// Stuck!
}
The issue: You're forced into a rigid hierarchy. You can't mix and match behaviors.
Composition: The "Has-A" Relationship
Composition models a "has-a" relationship by combining smaller objects.
// Behavior objects (composable)
const canEat = (state) => ({
eat: () => console.log(`${state.name} is eating`)
});
const canSleep = (state) => ({
sleep: () => console.log(`${state.name} is sleeping`)
});
const canBark = (state) => ({
bark: () => console.log(`${state.name} barks`)
});
const canFly = (state) => ({
fly: () => console.log(`${state.name} flies`)
});
// Compose behaviors
const createDog = (name) => {
const state = { name };
return {
...canEat(state),
...canSleep(state),
...canBark(state)
};
};
const createFlyingDog = (name) => {
const state = { name };
return {
...canEat(state),
...canSleep(state),
...canBark(state),
...canFly(state) // Easy to add!
};
};
const dog = createDog('Buddy');
dog.eat(); // "Buddy is eating"
dog.bark(); // "Buddy barks"
const flyingDog = createFlyingDog('Super Buddy');
flyingDog.bark(); // "Super Buddy barks"
flyingDog.fly(); // "Super Buddy flies"
The relationship: Dog has the ability to eat, sleep, and bark. FlyingDog has those abilities plus flying.
Key Advantage: You can mix and match behaviors freely without rigid hierarchies.
Part 2: Why React Prefers Composition
React's component model is built around composition, not inheritance.
Inheritance in React (The Old Way)
In early React, you might see:
class BaseButton extends React.Component {
handleClick = () => {
console.log('Base button clicked');
}
render() {
return <button onClick={this.handleClick}>Base</button>;
}
}
class PrimaryButton extends BaseButton {
handleClick = () => {
console.log('Primary button clicked');
}
render() {
return (
<button className="primary" onClick={this.handleClick}>
Primary
</button>
);
}
}
Problems:
- Tight coupling —
PrimaryButtonis tightly bound toBaseButton's implementation - Hard to modify — Changing
BaseButtonmight breakPrimaryButton - Limited reuse — Can't easily combine behaviors from multiple base classes
- Not composable — Can't use a
PrimaryButtoninside aBaseButton
Composition in React (The Modern Way)
Instead of extending, compose:
function Button({ className, onClick, children }) {
return (
<button className={className} onClick={onClick}>
{children}
</button>
);
}
function PrimaryButton({ onClick, children }) {
return (
<Button className="primary" onClick={onClick}>
{children}
</Button>
);
}
function DangerButton({ onClick, children }) {
return (
<Button className="danger" onClick={onClick}>
{children}
</Button>
);
}
Advantages:
- Flexible — Easy to compose buttons with different styles
- Reusable —
Buttonis a simple, focused component - Testable — Each component can be tested independently
- Explicit — Clear what props each component accepts
Part 3: Composition Patterns in React
Pattern 1: children Prop (Containment)
The most basic composition pattern is using the children prop:
function Card({ children }) {
return <div className="card">{children}</div>;
}
function App() {
return (
<Card>
<h1>Title</h1>
<p>Description</p>
</Card>
);
}
Key Point: Card doesn't know what its children are — it just renders them. This makes it maximally reusable.
Pattern 2: Named Slots (Multiple Composition Points)
For more complex layouts, use named props instead of just children:
function Dialog({ title, content, actions }) {
return (
<div className="dialog">
<div className="dialog-title">{title}</div>
<div className="dialog-content">{content}</div>
<div className="dialog-actions">{actions}</div>
</div>
);
}
function App() {
return (
<Dialog
title={<h1>Confirm Delete</h1>}
content={<p>Are you sure?</p>}
actions={
<>
<button>Cancel</button>
<button>Delete</button>
</>
}
/>
);
}
Advantages:
- Clear structure (title, content, actions)
- Each slot can contain any React element
- More controlled than a single
childrenprop
Pattern 3: Specialization (Specific Cases of Generic Components)
Create specific versions of generic components:
function Dialog({ title, children }) {
return (
<div className="dialog">
<h1>{title}</h1>
<div>{children}</div>
</div>
);
}
function WelcomeDialog() {
return (
<Dialog title="Welcome">
<p>Thank you for visiting our spacecraft!</p>
</Dialog>
);
}
function ConfirmDialog() {
return (
<Dialog title="Confirm">
<p>Are you sure?</p>
<button>Yes</button>
<button>No</button>
</Dialog>
);
}
Key Point: WelcomeDialog and ConfirmDialog are specialized versions of the generic Dialog component.
Pattern 4: Higher-Order Components (HOCs)
HOCs are functions that take a component and return a new component with additional props or behavior:
// HOC that adds loading state
function withLoading(Component) {
return function WithLoadingComponent({ isLoading, ...props }) {
if (isLoading) {
return <div>Loading...</div>;
}
return <Component {...props} />;
};
}
function UserList({ users }) {
return (
<ul>
{users.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}
const UserListWithLoading = withLoading(UserList);
function App() {
const [users, setUsers] = useState([]);
const [isLoading, setIsLoading] = useState(true);
return <UserListWithLoading isLoading={isLoading} users={users} />;
}
Use case: Adding cross-cutting concerns (loading, error handling, authentication) to multiple components.
Pattern 5: Render Props
A component with a render prop takes a function as a child:
function MouseTracker({ render }) {
const [position, setPosition] = useState({ x: 0, y: 0 });
useEffect(() => {
const handleMouseMove = (e) => {
setPosition({ x: e.clientX, y: e.clientY });
};
window.addEventListener('mousemove', handleMouseMove);
return () => window.removeEventListener('mousemove', handleMouseMove);
}, []);
return render(position);
}
function App() {
return (
<MouseTracker
render={({ x, y }) => (
<div>
Mouse position: {x}, {y}
</div>
)}
/>
);
}
Use case: Sharing stateful logic between components without HOCs.
Modern alternative: Custom hooks (see below).
Pattern 6: Custom Hooks (Modern Composition)
Custom hooks are the modern way to compose logic:
function useMousePosition() {
const [position, setPosition] = useState({ x: 0, y: 0 });
useEffect(() => {
const handleMouseMove = (e) => {
setPosition({ x: e.clientX, y: e.clientY });
};
window.addEventListener('mousemove', handleMouseMove);
return () => window.removeEventListener('mousemove', handleMouseMove);
}, []);
return position;
}
function App() {
const { x, y } = useMousePosition();
return (
<div>
Mouse position: {x}, {y}
</div>
);
}
Advantages over HOCs and Render Props:
- Simpler syntax
- No wrapper components
- Easy to compose multiple hooks
- Clear dependencies
This is why React recommends hooks over HOCs and render props.
Part 4: Composing Hooks
Custom hooks can be composed just like components:
function useAuth() {
const [user, setUser] = useState(null);
useEffect(() => {
// Fetch user from API
const fetchUser = async () => {
const userData = await api.getUser();
setUser(userData);
};
fetchUser();
}, []);
return { user, isAuthenticated: !!user };
}
function usePermissions(user) {
const [permissions, setPermissions] = useState([]);
useEffect(() => {
if (user) {
const fetchPermissions = async () => {
const perms = await api.getPermissions(user.id);
setPermissions(perms);
};
fetchPermissions();
}
}, [user]);
return permissions;
}
function useDashboard() {
const { user, isAuthenticated } = useAuth(); // Compose hooks!
const permissions = usePermissions(user);
return { user, isAuthenticated, permissions };
}
function Dashboard() {
const { user, isAuthenticated, permissions } = useDashboard();
if (!isAuthenticated) {
return <div>Please log in</div>;
}
return (
<div>
<h1>Welcome, {user.name}!</h1>
<p>Permissions: {permissions.join(', ')}</p>
</div>
);
}
Key Point: Hooks compose horizontally (call multiple hooks), while inheritance composes vertically (extend classes).
Part 5: When Inheritance Might Make Sense (Rarely)
There are a few cases where inheritance is acceptable in JavaScript/React:
1. Extending Built-in Classes
class MyArray extends Array {
first() {
return this[0];
}
last() {
return this[this.length - 1];
}
}
const arr = new MyArray(1, 2, 3);
console.log(arr.first()); // 1
console.log(arr.last()); // 3
Use case: Adding utility methods to built-in types.
2. Third-Party Library Requirements
Some libraries require extending their classes:
import { Component } from 'some-library';
class MyComponent extends Component {
// Library requires extending Component
}
But: In React, you rarely need to extend React.Component anymore — use functional components instead.
Part 6: Composition Anti-Patterns
Anti-Pattern 1: Prop Drilling
Passing props through many levels:
function App() {
const user = { name: 'Alice' };
return <Layout user={user} />;
}
function Layout({ user }) {
return <Sidebar user={user} />;
}
function Sidebar({ user }) {
return <UserMenu user={user} />;
}
function UserMenu({ user }) {
return <div>{user.name}</div>;
}
Problem: Every component needs to pass user down, even if it doesn't use it.
Solution: Use Context API or state management:
const UserContext = React.createContext();
function App() {
const user = { name: 'Alice' };
return (
<UserContext.Provider value={user}>
<Layout />
</UserContext.Provider>
);
}
function UserMenu() {
const user = useContext(UserContext); // No prop drilling!
return <div>{user.name}</div>;
}
Anti-Pattern 2: Over-Abstraction
Creating too many generic components:
function GenericContainer({ type, className, style, children, ...props }) {
const Component = type || 'div';
return (
<Component className={className} style={style} {...props}>
{children}
</Component>
);
}
// Usage becomes confusing
<GenericContainer type="section" className="container" style={{ padding: 10 }}>
Content
</GenericContainer>
Problem: Too generic — harder to understand than just using <section>.
Solution: Create specific, purposeful components:
function Section({ children }) {
return <section className="container">{children}</section>;
}
Quick Reference Cheat Sheet
| Pattern | Use Case | Example |
|---|---|---|
children prop |
Containment, generic wrappers | <Card>{content}</Card> |
| Named props | Multiple composition points | <Dialog title={...} content={...} /> |
| Specialization | Specific versions of generic components |
<PrimaryButton> from <Button>
|
| HOCs | Adding behavior to multiple components | withLoading(Component) |
| Render Props | Sharing stateful logic | <Tracker render={data => ...} /> |
| Custom Hooks | Modern logic composition |
useAuth(), useMousePosition()
|
Key Takeaways
Composition is more flexible than inheritance — you can mix and match behaviors
React prefers composition — use children props, HOCs, render props, and hooks
Inheritance creates tight coupling — hard to change and reuse
Custom hooks are the modern way to compose logic — simpler than HOCs and render props
Use the children prop for containment — makes components maximally reusable
Avoid deep component hierarchies — prefer flat composition
Context API solves prop drilling — no need to pass props through every level
Interview Tip
When asked about composition vs inheritance:
- "Composition means building complex components by combining simpler ones, while inheritance means creating hierarchies of classes"
- Explain the problem: "Inheritance creates rigid hierarchies that are hard to change. If you need to mix behaviors from multiple base classes, you're stuck."
- React's approach: "React uses composition patterns like
childrenprops, HOCs, render props, and now custom hooks to share logic" - Best practice: "Use custom hooks to compose logic and the
childrenprop to compose UI" - Example: "Instead of extending a
BaseButtonclass, create aButtoncomponent that accepts props, then createPrimaryButtonthat rendersButtonwith specific props"
Now go forth and compose fearlessly!
Top comments (0)