React Composition vs Inheritance
Component composition, children prop, slots pattern, and why inheritance is avoided..
20+ years shipping production JavaScript and front-end systems at scale. Written from production experience, not tutorials.
- ✓JavaScript fundamentals (ES6 classes, prototypes, functions), Node.js v18+, basic understanding of OOP concepts (classes, objects, inheritance), familiarity with testing concepts (mocking, unit tests)
React Composition vs Inheritance: A core React concept for building modern user interfaces. It helps you structure your components efficiently and handle data flow predictably.
React is a JavaScript library for building user interfaces. This article covers composition — a key concept for building modern web applications.
| Chrome | Firefox | Safari | Edge |
|---|---|---|---|
| ✓ | ✓ | ✓ | ✓ |
A comprehensive guide to react composition vs inheritance with production examples and best practices.
The Core Question: Inheritance or Composition?
In object-oriented programming, inheritance and composition are two fundamental techniques for reusing code and establishing relationships between classes. Inheritance creates an "is-a" relationship (e.g., a Dog is an Animal), while composition creates a "has-a" relationship (e.g., a Car has an Engine). The choice between them has profound implications for code maintainability, flexibility, and testability. In JavaScript, where prototypal inheritance is the native mechanism, composition often emerges as the more pragmatic choice, especially in production systems that must evolve over time. This article dissects both approaches with real-world examples, highlighting when each is appropriate and why composition tends to win in large-scale applications.
Inheritance in JavaScript: Prototypal Chains
JavaScript's inheritance is prototypal: objects inherit from other objects via prototype chains. The class syntax is syntactic sugar over prototypes. When you extend a class, the child's prototype points to the parent's prototype. This chain allows method lookup but also introduces coupling. In production, deep inheritance hierarchies (more than 2-3 levels) become hard to debug and refactor. For example, changing a method in a base class can break subclasses that override it, especially if the base method's contract changes. The classic 'fragile base class' problem is real: a seemingly safe change in a parent can cause runtime errors in children that were never tested with the new behavior.
Composition: Building with Small, Focused Objects
Composition assembles behavior by combining small, independent objects. Each object has a single responsibility. Instead of inheriting from a base class, you compose objects that delegate to each other. This approach aligns with the Single Responsibility Principle and makes testing easier: you can mock or replace composed objects without affecting the consumer. In production, composition shines when requirements change frequently. For example, a user object might need different authentication strategies (OAuth, JWT, SAML) depending on the environment. With composition, you inject the appropriate strategy at runtime. Inheritance would require creating subclasses for each combination, leading to a combinatorial explosion.
When Inheritance Makes Sense: Stable Hierarchies
Inheritance is not always bad. It works well when the hierarchy is stable and unlikely to change. For example, UI component frameworks often use inheritance: a Button extends a BaseComponent. The base provides shared lifecycle methods, and subclasses add specific rendering. In production, inheritance is acceptable when the base class is 'frozen'—no new methods are added, and existing methods have stable contracts. Also, inheritance is useful for frameworks that require subclassing to provide hooks. However, even in these cases, prefer shallow hierarchies (max 2 levels) and document the contract explicitly. The key is to recognize when the 'is-a' relationship is truly invariant.
Mixins: A Middle Ground in JavaScript
Mixins provide a way to reuse behavior across unrelated classes without inheritance. A mixin is a function that takes a class and returns an extended class with additional methods. This is composition at the class level. In JavaScript, mixins are implemented using class expressions and Object.assign. They avoid the diamond problem (multiple inheritance ambiguity) because each mixin is independent. However, mixins can cause name collisions if two mixins define the same method. In production, mixins are useful for cross-cutting concerns like logging, serialization, or event handling. They are more flexible than inheritance but less structured than pure composition.
Practical Comparison: Building a Notification System
Let's compare inheritance and composition by building a notification system that supports email, SMS, and push notifications. With inheritance, you'd create a base Notifier class and extend it for each channel. But what if you need to send notifications via multiple channels? You'd need a MultiChannelNotifier subclass, leading to a combinatorial explosion. With composition, you create separate EmailNotifier, SMSNotifier, and PushNotifier objects, and a NotificationService that composes them. Adding a new channel (e.g., Slack) requires no changes to existing classes—just create a new notifier and inject it. This is the Open/Closed Principle in action: open for extension, closed for modification.
Testing: Composition Wins Hands Down
Testing is where composition truly shines. With inheritance, testing a subclass often requires instantiating the base class, which may have complex dependencies. Mocking becomes difficult because the base class methods are tightly coupled. With composition, you can easily mock or stub the composed objects. For example, to test a Car that has an Engine, you can inject a mock engine that returns a fixed value. This isolates the test to the Car logic. In production, this means faster test suites and fewer flaky tests. Inheritance tests tend to be integration tests by nature, while composition enables true unit tests.
Performance Considerations: Prototype Lookup vs Object Composition
Inheritance leverages the prototype chain for method lookup, which is highly optimized in JavaScript engines. Composition uses direct property access, which is also fast. In practice, the performance difference is negligible for most applications. However, deep inheritance chains (more than 5 levels) can cause slower property lookups due to chain traversal. Composition with many small objects can increase memory usage due to object overhead. In production, profile your specific use case. For hot paths (e.g., rendering loops), consider using plain objects with shared functions (like the module pattern) to avoid both inheritance and composition overhead.
Real-World Failure: The Fragile Base Class in Production
A common production failure is the 'fragile base class' problem. Consider a team that built a UI component library with a deep inheritance hierarchy: BaseComponent -> InteractiveComponent -> FormComponent -> InputField. A developer adds a method to validate()BaseComponent to handle generic validation. However, InputField already has a method with different semantics. The new base method overrides the child's method, breaking form validation across the application. This bug was caught in production because the change was assumed safe. The fix: refactor to composition, where each component has its own validator object, avoiding method collisions.validate()
Decision Framework: How to Choose
When faced with a design decision between inheritance and composition, ask: Is the relationship truly 'is-a' and invariant? If yes, and the hierarchy is shallow (max 2 levels), inheritance might be appropriate. Otherwise, prefer composition. Also consider: Will the behavior change independently? If you need to swap or extend behavior at runtime, composition is the only choice. Will you need to test components in isolation? Composition makes that easy. Is the base class likely to change? If yes, composition isolates changes. Finally, consider the team's familiarity. Composition is more explicit and easier to reason about for most developers.
Modern JavaScript: Composition with Functional Programming
Modern JavaScript embraces functional programming patterns that naturally favor composition. Functions can be composed using pipe or compose utilities. Instead of objects, you can use closures to encapsulate state and behavior. This approach avoids the this binding issues and prototype chain complexity. For example, a notifier can be a function that takes a message and sends it via a channel function. This is the ultimate form of composition: pure functions combined. In production, functional composition leads to highly testable and predictable code. Libraries like Ramda and Lodash/fp provide utilities for function composition.
Summary: Composition is the Default Choice
After examining both approaches, the conclusion is clear: composition should be your default choice. It provides better flexibility, testability, and maintainability. Inheritance has its place in stable, shallow hierarchies, but those cases are rare in evolving production systems. The JavaScript ecosystem has moved toward composition: React uses composition over inheritance for components; Node.js modules are composed via require/import. By favoring composition, you build systems that are easier to change, test, and reason about. Remember: inheritance is a detail, composition is a principle.
React Component Composition: The Mental Shift
React's design philosophy centers on composition over inheritance. Unlike traditional OOP where you extend base classes to reuse behavior, React encourages building UIs by composing small, independent components. This shift is crucial because React components are functions, not classes—they receive props and return elements. Inheritance would create tight coupling and fragile hierarchies, while composition allows flexible, reusable code. For example, instead of creating a Button base class and extending it for PrimaryButton and IconButton, you compose a generic Button component that accepts variant and icon props. This mental shift from "is-a" to "has-a" relationships leads to more maintainable and testable code. React's component model inherently supports composition through props, children, and higher-order patterns, making inheritance largely unnecessary.
Slots Pattern: Multiple Named Children
The slots pattern allows you to pass multiple distinct JSX sections to a component via named props, rather than relying on a single children prop. This is useful when a component has multiple placeholders for content, such as a card with a header, body, and footer. Instead of using complex conditional logic, you define named props like header, body, and footer that accept React nodes. This pattern improves readability and flexibility, as each slot can be independently customized. For example, a Card component can accept header, body, and footer slots, allowing consumers to compose the card's layout without modifying the component internals. This is a direct application of composition, where the parent component controls the structure by providing different pieces.
Compound Components with Implicit State
Compound components are a pattern where a set of components work together to share implicit state via React Context. This is common in UI components like Tabs or Accordion, where the parent manages state (e.g., active tab) and child components (e.g., Tab, TabPanel) access it without prop drilling. The parent component provides a context that holds the state and updater functions. Children use useContext to read and modify the state. This pattern promotes composition by allowing users to arrange components declaratively while the internal state is managed implicitly. For example, a Tabs component can wrap Tab and TabPanel components, and the active tab state is shared via context, eliminating the need for manual prop passing.
Custom Hooks as the Modern Composition Default
Custom hooks have become the primary way to compose logic in React, replacing older patterns like Higher-Order Components (HOCs) and render props. Hooks allow you to extract stateful logic into reusable functions that can be composed within functional components. Unlike HOCs, which wrap components and can lead to wrapper hell, or render props, which require nesting, hooks are flat and composable. For example, a useWindowSize hook can be used in any component to track window dimensions, and multiple hooks can be combined in a single component without nesting. This makes code more readable and easier to test. Custom hooks are the modern default for sharing non-visual logic, aligning with React's functional composition philosophy.
Polymorphic Components with as Prop
Polymorphic components allow you to change the underlying HTML element or component rendered by a component via an as prop. This is useful for creating flexible UI primitives like buttons that can render as <button>, <a>, or even a custom Link component from a router. The component uses the as prop to determine the root element, while passing all other props to it. This pattern leverages composition by allowing consumers to decide the semantic element without creating multiple variants. For example, a Button component can accept an as prop to render as a <button> by default, or as a <a> for links, or as a Link from React Router. This reduces duplication and enhances reusability.
as prop allow rendering different HTML elements or components while sharing common props, promoting composition and reducing code duplication.| File | Command / Code | Purpose |
|---|---|---|
| inheritance-vs-composition.js | class Animal { | The Core Question |
| prototypal-inheritance.js | function Animal(name) { | Inheritance in JavaScript |
| composition-example.js | class Authenticator { | Composition |
| ui-inheritance.js | class BaseComponent { | When Inheritance Makes Sense |
| mixin-example.js | const LoggerMixin = (Base) => class extends Base { | Mixins |
| notification-system.js | class EmailNotifier { | Practical Comparison |
| testing-composition.js | class Car { | Testing |
| performance-comparison.js | class A { | Performance Considerations |
| fragile-base-class.js | class BaseComponent { | Real-World Failure |
| decision-framework.js | function chooseDesign(relationship, hierarchyDepth, behaviorChange, testIsolatio... | Decision Framework |
| functional-composition.js | const pipe = (...fns) => (x) => fns.reduce((v, f) => f(v), x); | Modern JavaScript |
| summary.js | function Button({ onClick, children }) { | Summary |
| ButtonComposition.jsx | function Button({ variant, icon, children }) { | React Component Composition |
| SlotsPattern.jsx | function Card({ header, body, footer }) { | Slots Pattern |
| CompoundTabs.jsx | const TabsContext = React.createContext(); | Compound Components with Implicit State |
| useWindowSize.js | function useWindowSize() { | Custom Hooks as the Modern Composition Default |
| PolymorphicButton.jsx | function Button({ as: Component = 'button', children, ...props }) { | Polymorphic Components with as Prop |
Key takeaways
Common mistakes to avoid
3 patternsNot understanding React component re-rendering
Ignoring the rules of hooks
Mutating state directly instead of using setState
Interview Questions on This Topic
What is the Virtual DOM and how does React use it?
Frequently Asked Questions
20+ years shipping production JavaScript and front-end systems at scale. Written from production experience, not tutorials.
That's React. Mark it forged?
7 min read · try the examples if you haven't