Introduction
React is built on the philosophy of building user interfaces using components. One of the key decisions developers face when designing React applications is how to reuse code effectively. In traditional object-oriented programming (OOP), inheritance is commonly used for code reuse. However, React encourages composition over inheritance as the preferred pattern for creating flexible, maintainable, and scalable components.
This post explores the concepts of composition and inheritance, their differences, advantages, and best practices in React. We will cover real-world examples, patterns, and guidelines to help you make the right choice when building React applications.
Understanding Inheritance in Traditional Programming
Inheritance is an OOP concept where one class derives from another class, inheriting its properties and methods. It allows code reuse but introduces tight coupling between parent and child classes.
Example in JavaScript
class Vehicle {
start() {
console.log("Vehicle started");
}
}
class Car extends Vehicle {
drive() {
console.log("Car is driving");
}
}
const myCar = new Car();
myCar.start(); // Vehicle started
myCar.drive(); // Car is driving
Inheritance works well in OOP, but in React, it is less favored because it can lead to rigid component hierarchies and make components harder to reuse flexibly.
Understanding Composition in React
Composition is a pattern where components contain other components or accept children and props to assemble complex UIs. Instead of inheriting behavior, components compose behavior by combining smaller, independent components.
Example: Basic Composition
function Card({ title, children }) {
return (
<div className="card">
<h2>{title}</h2>
<div>{children}</div>
</div>
);
}
function App() {
return (
<Card title="Profile">
<p>Name: John Doe</p>
<p>Email: [email protected]</p>
</Card>
);
}
Here, the Card component is composed with children. You can reuse it for multiple purposes by passing different children.
Why React Prefers Composition Over Inheritance
- Flexibility: Composition allows components to be combined in various ways, while inheritance locks behavior in a hierarchy.
- Reusability: Small, composable components can be reused across the application without creating deep hierarchies.
- Maintainability: Changes in one component do not break the behavior of others as long as props and children are used correctly.
- Simplicity: Composition avoids the complexity of managing parent-child class relationships.
- Aligns with React Philosophy: React’s one-way data flow and declarative UI design are better suited to composition.
Comparison Between Composition and Inheritance
| Feature | Inheritance | Composition |
|---|---|---|
| Approach | Hierarchical, class-based | Modular, component-based |
| Reusability | Limited, tightly coupled | High, flexible and decoupled |
| Flexibility | Less flexible, rigid hierarchies | Highly flexible, mix and match |
| Complexity | Can become complex with deep hierarchies | Simple, promotes modularity |
| React Alignment | Not recommended | Recommended |
Using Props for Composition
Props are fundamental to composition. By passing props, components can behave differently without inheriting from a parent.
Example: Passing Props
function Button({ label, onClick, style }) {
return (
<button style={style} onClick={onClick}>
{label}
</button>
);
}
function App() {
return (
<div>
<Button label="Save" onClick={() => alert("Saved!")} style={{ color: 'green' }} />
<Button label="Delete" onClick={() => alert("Deleted!")} style={{ color: 'red' }} />
</div>
);
}
Using Children for Composition
The children prop allows components to wrap other components and display dynamic content, making them highly reusable.
function Modal({ title, children }) {
return (
<div className="modal">
<h2>{title}</h2>
<div>{children}</div>
</div>
);
}
function App() {
return (
<Modal title="User Info">
<p>Name: Alice</p>
<p>Email: [email protected]</p>
</Modal>
);
}
Higher-Order Components (HOCs)
Higher-order components are functions that take a component and return a new component, allowing behavior reuse without inheritance.
Example: HOC for Logging
function withLogging(WrappedComponent) {
return function(props) {
console.log("Rendering:", WrappedComponent.name);
return <WrappedComponent {...props} />;
};
}
function Button({ label }) {
return <button>{label}</button>;
}
const ButtonWithLogging = withLogging(Button);
function App() {
return <ButtonWithLogging label="Click Me" />;
}
HOCs are a form of composition because they wrap and enhance existing components.
Render Props Pattern
Render props allow passing a function as a prop to control what a component renders, enabling flexible composition.
Example: Render Props
function DataProvider({ render }) {
const data = ["Apple", "Banana", "Cherry"];
return render(data);
}
function App() {
return (
<DataProvider render={(fruits) => (
<ul>
{fruits.map((fruit, index) => <li key={index}>{fruit}</li>)}
</ul>
)} />
);
}
Avoiding Inheritance in React
Although React supports extending classes, it is rarely necessary. Instead of creating a base component and extending it, you should:
- Create small, focused components.
- Combine components using composition.
- Share behavior using HOCs, render props, or custom hooks.
Example: What Not to Do
class BaseButton extends React.Component {
render() {
return <button>{this.props.label}</button>;
}
}
class SaveButton extends BaseButton {
render() {
return <button style={{ color: 'green' }}>{this.props.label}</button>;
}
}
This approach tightly couples components and limits flexibility. Composition is a better choice.
Custom Hooks for Composition
React Hooks provide a function-based way to reuse behavior across components.
Example: useCounter Hook
import { useState } from 'react';
function useCounter(initialValue = 0) {
const [count, setCount] = useState(initialValue);
const increment = () => setCount(count + 1);
const decrement = () => setCount(count - 1);
return { count, increment, decrement };
}
function Counter() {
const { count, increment, decrement } = useCounter(0);
return (
<div>
<p>Count: {count}</p>
<button onClick={increment}>+</button>
<button onClick={decrement}>-</button>
</div>
);
}
Custom hooks are a modern form of composition for sharing stateful logic.
Real-World Example: Building a Dashboard
Consider a dashboard with multiple widgets. Using composition:
function Widget({ title, children }) {
return (
<div className="widget">
<h3>{title}</h3>
<div>{children}</div>
</div>
);
}
function Dashboard() {
return (
<div className="dashboard">
<Widget title="User Stats">
<p>Active Users: 150</p>
</Widget>
<Widget title="Revenue">
<p>$5000 this month</p>
</Widget>
<Widget title="Notifications">
<ul>
<li>New signup: John</li>
<li>Server maintenance at 8 PM</li>
</ul>
</Widget>
</div>
);
}
Each widget is independent and reusable, and behavior can be added without modifying existing components.
Best Practices for Composition in React
- Keep components small and focused.
- Use props and children for flexible configuration.
- Use custom hooks for reusable stateful logic.
- Avoid deep inheritance hierarchies.
- Use HOCs and render props for shared behavior.
- Favor composition over inheritance for maintainability and flexibility.
Leave a Reply