Three years ago, I inherited a React codebase where every button extended a BaseButton class. Sounds reasonable, right?
The team had created this beautiful inheritance hierarchy: BaseButton → PrimaryButton → SubmitButton → FormSubmitButton. Clean OOP principles at work.
Then the design system changed. One small tweak to BaseButton, and 47 components broke across 12 different screens.
That's when I learned why React fights inheritance so hard.
React's approach is fundamentally different. Instead of "is-a" relationships, React thinks in "has-a" terms.
The React team discovered something crucial:
"UI components don't need deep inheritance trees. They need flexible composition patterns."
After refactoring that codebase using composition, we reduced component coupling by 80% and cut our bug reports in half.
Why Inheritance Feels Natural (But Isn't)
Inheritance works well for modeling real-world hierarchies. A Dog is an Animal. A Car is a Vehicle. Makes sense.
In traditional OOP:
class Animal {
void speak() { System.out.println("I make noise"); }
}
class Dog extends Animal {
void speak() { System.out.println("Bark"); }
}
But UI components aren't hierarchical by nature. A Modal isn't really a "type of" Container. It's a Container that contains specific functionality.
Composition: Building with Contracts, Not Classes
Composition in React means components accept other components as children or props to build complex UIs from simple, focused pieces.
Instead of rigid inheritance:
<Dialog>
<DialogHeader>
<DialogTitle>Confirm Action</DialogTitle>
</DialogHeader>
<DialogContent>
<p>Are you sure you want to delete this item?</p>
</DialogContent>
<DialogActions>
<Button variant="secondary">Cancel</Button>
<Button variant="danger">Delete</Button>
</DialogActions>
</Dialog>
Each component has a single responsibility and clear API boundaries. The Dialog doesn't need to know about Button implementations, and Button doesn't need to know about Dialog styling.
👉 You compose behavior instead of inheriting it.
⚔️ Real-World Impact: Composition vs Inheritance
| Scenario | Composition (React way) | Inheritance (OOP style) |
|---|---|---|
| Design System Updates | Change one component, others unaffected | Change base class, all children potentially break |
| Testing Strategy | Test components in isolation with mock props | Must test entire inheritance chain |
| Team Collaboration | Multiple devs can work on different components safely | Changes to base components require team coordination |
| Bundle Size | Tree-shaking removes unused code effectively | Base classes often bring unnecessary methods |
| Production Debugging | ✅ Clear component boundaries in DevTools | ❌ Complex inheritance chains are hard to trace |
The Technical Reality: Why React Abandoned Inheritance
Early React actually supported mixins (a form of inheritance). The React team removed them because they consistently led to these problems:
- Name collisions: Multiple mixins defining the same method
- Implicit dependencies: Mixins depending on other mixins without clear contracts
- Snowball complexity: Simple components becoming impossible to understand
Modern React architecture principles:
- Single Responsibility: Each component does one thing well
- Explicit Dependencies: All inputs come through props or hooks
- Composition Over Configuration: Build complex UIs by combining simple parts
This isn't just philosophy—it's proven architecture that scales to codebases with thousands of components.
Production-Ready Composition Pattern
// Flexible layout component that adapts to content
const Dashboard = ({ user, notifications, children }) => {
return (
<AppLayout>
<Sidebar>
<UserProfile user={user} />
<NavigationMenu />
</Sidebar>
<MainContent>
<TopBar notifications={notifications} />
{children}
</MainContent>
</AppLayout>
);
};
// Usage - each dashboard page composes different content
<Dashboard user={currentUser} notifications={alerts}>
<AnalyticsView />
</Dashboard>
<Dashboard user={currentUser} notifications={alerts}>
<SettingsPanel />
</Dashboard>
Notice how Dashboard doesn't need to know about AnalyticsView or SettingsPanel implementations. Each component has clear boundaries and can be developed, tested, and optimized independently.
The Inheritance Trap (What We Used to Do)
// This approach seems logical at first...
class BaseButton extends React.Component {
render() {
return (
<button
className={`btn ${this.props.className || ''}`}
onClick={this.props.onClick}
>
{this.props.children}
</button>
);
}
}
class PrimaryButton extends BaseButton {
render() {
return (
<button
className={`btn btn-primary ${this.props.className || ''}`}
onClick={this.props.onClick}
>
{this.props.children}
</button>
);
}
}
// ...but what happens when you need a primary button with an icon?
class IconPrimaryButton extends PrimaryButton {
// Now you're duplicating render logic and fighting the inheritance chain
}
Six months later, you'll have 15 button classes and no one will remember which one to use for which scenario. I've debugged this exact problem more times than I'd like to admit.
Decision Framework: When to Use What
✅ Use composition when: (99% of cases)
- Building UI layouts and component libraries
- Sharing logic between components (use custom hooks instead)
- Creating reusable patterns (modals, forms, cards)
- You want predictable, testable code
⚠️ Consider inheritance only when: (rare in React)
- Wrapping third-party class-based libraries
- Building abstract base classes for complex state machines
- You have a clear "is-a" relationship that won't change
Composition Patterns I Use Daily
1. Render Props for Logic Sharing
const DataFetcher = ({ url, children }) => {
const [data, loading, error] = useFetch(url);
return children({ data, loading, error });
};
// Usage across different UIs
<DataFetcher url="/api/users">
{({ data, loading }) => loading ? <Spinner /> : <UserList users={data} />}
</DataFetcher>
2. Compound Components for Complex UIs
<Tabs>
<TabList>
<Tab>Overview</Tab>
<Tab>Analytics</Tab>
</TabList>
<TabPanels>
<TabPanel><Overview /></TabPanel>
<TabPanel><Analytics /></TabPanel>
</TabPanels>
</Tabs>
3. Higher-Order Components for Cross-Cutting Concerns
const withErrorBoundary = (Component) => (props) => (
<ErrorBoundary>
<Component {...props} />
</ErrorBoundary>
);
const SafeUserProfile = withErrorBoundary(UserProfile);
Real-World Advice
After building component libraries for teams ranging from 5 to 500 developers, here's what I've learned works:
- Start with the smallest possible components - you can always compose them into larger ones
- Make your component APIs explicit - if it takes more than 10 props, break it down
- Use TypeScript - composition benefits massively from good type definitions
- Document composition patterns - show examples of how components work together
If you're using my Virendana UI library, you'll notice every component follows these composition principles.
Need a custom button? Don't extend Button—compose it:
<Button variant="primary" size="lg">
<Icon name="save" />
Save Changes
</Button>
Everything is composable by design, because that's how React components should work.
The Bottom Line: Why Composition Wins
| Metric | Composition | Inheritance |
|---|---|---|
| React Philosophy | ✅ Core principle since React 0.14 | ❌ Deprecated (mixins removed) |
| Team Productivity | High - parallel development, clear boundaries | Low - coordination overhead, merge conflicts |
| Maintenance Cost | Low - isolated changes, predictable impact | High - cascading changes, hard to predict side effects |
| Performance | Optimized - React's reconciliation works best here | Suboptimal - deep trees harder to optimize |
| Industry Standard | Yes - used by all major React libraries | No - actively discouraged in style guides |
FAQs
1. Why does React prefer composition over inheritance?
React's virtual DOM and reconciliation algorithm work best with component trees that have clear parent-child relationships through props and children, not class hierarchies. Composition makes React's optimization strategies more effective.
2. Is it bad to use inheritance in React?
Not "bad," but it goes against React's grain. You'll fight the framework instead of working with it. Modern React patterns (hooks, context, composition) solve the same problems more elegantly.
3. How do I share styles across components without inheritance?
Use CSS-in-JS libraries, CSS modules, or design tokens. Pass styling props to composed components. Create theme providers that inject consistent styles.
4. What about sharing logic between components?
Custom hooks are the modern solution. They're more flexible than inheritance and work with React's lifecycle better than mixins ever did.
5. How do I convince my team to adopt composition?
Start small—refactor one problematic inheritance chain to composition. Show the improved testability, easier debugging, and reduced coupling. Results speak louder than theory.