DEV Community

Tihomir Ivanov
Tihomir Ivanov

Posted on

Composition vs Inheritance: Why React Chose the "Has-A" Over "Is-A" Relationship

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"
Enter fullscreen mode Exit fullscreen mode

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 Robot that can walk() but doesn't need eat() or sleep()
  • A FlyingDog that can bark() and fly()
  • A SwimmingRobot that can walk() and swim()

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!
}
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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>
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

Problems:

  1. Tight coupling — PrimaryButton is tightly bound to BaseButton's implementation
  2. Hard to modify — Changing BaseButton might break PrimaryButton
  3. Limited reuse — Can't easily combine behaviors from multiple base classes
  4. Not composable — Can't use a PrimaryButton inside a BaseButton

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

Advantages:

  • Flexible — Easy to compose buttons with different styles
  • Reusable — Button is 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>
  );
}
Enter fullscreen mode Exit fullscreen mode

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>
        </>
      }
    />
  );
}
Enter fullscreen mode Exit fullscreen mode

Advantages:

  • Clear structure (title, content, actions)
  • Each slot can contain any React element
  • More controlled than a single children prop

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

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} />;
}
Enter fullscreen mode Exit fullscreen mode

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>
      )}
    />
  );
}
Enter fullscreen mode Exit fullscreen mode

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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>;
}
Enter fullscreen mode Exit fullscreen mode

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>;
}
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

Problem: Too generic — harder to understand than just using <section>.

Solution: Create specific, purposeful components:

function Section({ children }) {
  return <section className="container">{children}</section>;
}
Enter fullscreen mode Exit fullscreen mode

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:

  1. "Composition means building complex components by combining simpler ones, while inheritance means creating hierarchies of classes"
  2. 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."
  3. React's approach: "React uses composition patterns like children props, HOCs, render props, and now custom hooks to share logic"
  4. Best practice: "Use custom hooks to compose logic and the children prop to compose UI"
  5. Example: "Instead of extending a BaseButton class, create a Button component that accepts props, then create PrimaryButton that renders Button with specific props"

Now go forth and compose fearlessly!

Top comments (0)