Understanding Inheritance vs Composition
In programming, inheritance is like a family tree where children inherit traits from parents. Composition, on the other hand, is like assembling a car from parts - each part has a specific function and they work together to create the whole.
Problems with Inheritance in UI Development
1. The Fragile Base Class Problem
// ❌ Inheritance approach - fragile and rigid
class BaseButton extends React.Component {
render() {
return <button>{this.props.label}</button>;
}
}
class IconButton extends BaseButton {
render() {
// Need to override entire render method
return (
<button>
<Icon type={this.props.icon} />
{this.props.label}
</button>
);
}
}
class PrimaryButton extends BaseButton {
render() {
// Again, override entire render
return (
<button className="primary">
{this.props.label}
</button>
);
}
}
// What if we need a PrimaryIconButton? Multiple inheritance?
// This quickly becomes a mess!
2. The Diamond Problem
// ❌ Multiple inheritance creates ambiguity
// If JavaScript had multiple inheritance...
class FlyingCreature {
move() { return "flying"; }
}
class SwimmingCreature {
move() { return "swimming"; }
}
// Which move() method should Duck inherit?
class Duck extends FlyingCreature, SwimmingCreature {
// Ambiguous!
}
Composition: The React Way
Basic Composition Pattern
// ✅ Composition approach - flexible and clear
function Button({ children, className = '', ...props }) {
return (
<button className={`btn ${className}`} {...props}>
{children}
</button>
);
}
function Icon({ type }) {
return <i className={`icon icon-${type}`} />;
}
// Compose components together
function IconButton({ icon, children, ...props }) {
return (
<Button {...props}>
<Icon type={icon} />
{children}
</Button>
);
}
function PrimaryButton({ children, ...props }) {
return (
<Button className="primary" {...props}>
{children}
</Button>
);
}
// Now we can easily create combinations
function PrimaryIconButton({ icon, children, ...props }) {
return (
<PrimaryButton {...props}>
<Icon type={icon} />
{children}
</PrimaryButton>
);
}
Composition Patterns in React
1. Containment
// Components that don't know their children ahead of time
function Card({ children, title }) {
return (
<div className="card">
<div className="card-header">
<h3>{title}</h3>
</div>
<div className="card-body">
{children}
</div>
</div>
);
}
function Modal({ children, isOpen, onClose }) {
if (!isOpen) return null;
return (
<div className="modal-overlay" onClick={onClose}>
<div className="modal-content" onClick={e => e.stopPropagation()}>
{children}
</div>
</div>
);
}
// Usage
function App() {
return (
<Card title="User Profile">
<p>Name: John Doe</p>
<p>Email: john@example.com</p>
</Card>
);
}
2. Specialization
// Generic component
function Dialog({ title, message, onConfirm, onCancel }) {
return (
<div className="dialog">
<h2>{title}</h2>
<p>{message}</p>
<div className="dialog-buttons">
<button onClick={onCancel}>Cancel</button>
<button onClick={onConfirm}>Confirm</button>
</div>
</div>
);
}
// Specialized versions
function DeleteConfirmDialog({ itemName, onConfirm, onCancel }) {
return (
<Dialog
title="Confirm Deletion"
message={`Are you sure you want to delete ${itemName}?`}
onConfirm={onConfirm}
onCancel={onCancel}
/>
);
}
function WelcomeDialog({ userName }) {
return (
<Dialog
title="Welcome!"
message={`Hello ${userName}, welcome to our app!`}
onConfirm={() => console.log('OK clicked')}
onCancel={() => console.log('Cancel clicked')}
/>
);
}
3. Slots Pattern
// Layout with multiple "slots"
function PageLayout({ header, sidebar, content, footer }) {
return (
<div className="page">
<header className="page-header">
{header}
</header>
<div className="page-body">
<aside className="page-sidebar">
{sidebar}
</aside>
<main className="page-content">
{content}
</main>
</div>
<footer className="page-footer">
{footer}
</footer>
</div>
);
}
// Usage
function Dashboard() {
return (
<PageLayout
header={<Header user={currentUser} />}
sidebar={<Navigation items={menuItems} />}
content={<DashboardContent data={dashboardData} />}
footer={<Footer />}
/>
);
}
4. Render Props Pattern
// Component that shares its state via render prop
function MouseTracker({ render }) {
const [position, setPosition] = useState({ x: 0, y: 0 });
useEffect(() => {
const handleMouseMove = (event) => {
setPosition({
x: event.clientX,
y: event.clientY
});
};
window.addEventListener('mousemove', handleMouseMove);
return () => window.removeEventListener('mousemove', handleMouseMove);
}, []);
return render(position);
}
// Usage
function App() {
return (
<MouseTracker
render={({ x, y }) => (
<div>
Mouse position: {x}, {y}
</div>
)}
/>
);
}
// Alternative: children as function
function MouseTrackerAlt({ children }) {
const [position, setPosition] = useState({ x: 0, y: 0 });
// ... same logic
return children(position);
}
// Usage
<MouseTrackerAlt>
{({ x, y }) => <div>Mouse: {x}, {y}</div>}
</MouseTrackerAlt>
5. Higher-Order Components (HOCs)
// HOC adds functionality to wrapped component
function withAuth(WrappedComponent) {
return function AuthenticatedComponent(props) {
const { user, loading } = useAuth();
if (loading) return <LoadingSpinner />;
if (!user) return <Redirect to="/login" />;
return <WrappedComponent {...props} user={user} />;
};
}
// Usage
const ProtectedDashboard = withAuth(Dashboard);
// HOC for logging
function withLogging(WrappedComponent) {
return function LoggedComponent(props) {
useEffect(() => {
console.log(`${WrappedComponent.name} mounted`);
return () => console.log(`${WrappedComponent.name} unmounted`);
}, []);
return <WrappedComponent {...props} />;
};
}
// Compose multiple HOCs
const EnhancedComponent = withAuth(withLogging(Dashboard));
Advanced Composition Patterns
1. Compound Components
// Components that work together
const TabContext = createContext();
function Tabs({ children, defaultActiveTab }) {
const [activeTab, setActiveTab] = useState(defaultActiveTab);
return (
<TabContext.Provider value={{ activeTab, setActiveTab }}>
<div className="tabs">
{children}
</div>
</TabContext.Provider>
);
}
function TabList({ children }) {
return <div className="tab-list">{children}</div>;
}
function Tab({ id, children }) {
const { activeTab, setActiveTab } = useContext(TabContext);
return (
<button
className={`tab ${activeTab === id ? 'active' : ''}`}
onClick={() => setActiveTab(id)}
>
{children}
</button>
);
}
function TabPanels({ children }) {
return <div className="tab-panels">{children}</div>;
}
function TabPanel({ id, children }) {
const { activeTab } = useContext(TabContext);
if (activeTab !== id) return null;
return <div className="tab-panel">{children}</div>;
}
// Usage
function App() {
return (
<Tabs defaultActiveTab="home">
<TabList>
<Tab id="home">Home</Tab>
<Tab id="profile">Profile</Tab>
<Tab id="settings">Settings</Tab>
</TabList>
<TabPanels>
<TabPanel id="home">Home content</TabPanel>
<TabPanel id="profile">Profile content</TabPanel>
<TabPanel id="settings">Settings content</TabPanel>
</TabPanels>
</Tabs>
);
}
2. Provider Pattern
// Theme provider example
const ThemeContext = createContext();
function ThemeProvider({ children }) {
const [theme, setTheme] = useState('light');
const toggleTheme = () => {
setTheme(prev => prev === 'light' ? 'dark' : 'light');
};
const value = {
theme,
toggleTheme,
isDark: theme === 'dark'
};
return (
<ThemeContext.Provider value={value}>
<div className={`theme-${theme}`}>
{children}
</div>
</ThemeContext.Provider>
);
}
function useTheme() {
const context = useContext(ThemeContext);
if (!context) {
throw new Error('useTheme must be used within ThemeProvider');
}
return context;
}
// Usage
function ThemedButton() {
const { theme, toggleTheme } = useTheme();
return (
<button onClick={toggleTheme}>
Current theme: {theme}
</button>
);
}
Composition vs Inheritance: When to Use What
Real-World Examples
1. Form Builder
// Composable form components
function Form({ children, onSubmit }) {
const handleSubmit = (e) => {
e.preventDefault();
const formData = new FormData(e.target);
onSubmit(Object.fromEntries(formData));
};
return <form onSubmit={handleSubmit}>{children}</form>;
}
function Field({ label, name, type = 'text', ...props }) {
return (
<div className="field">
<label htmlFor={name}>{label}</label>
<input id={name} name={name} type={type} {...props} />
</div>
);
}
function Select({ label, name, options, ...props }) {
return (
<div className="field">
<label htmlFor={name}>{label}</label>
<select id={name} name={name} {...props}>
{options.map(option => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</select>
</div>
);
}
// Compose a registration form
function RegistrationForm() {
const handleSubmit = (data) => {
console.log('Form submitted:', data);
};
return (
<Form onSubmit={handleSubmit}>
<Field label="Username" name="username" required />
<Field label="Email" name="email" type="email" required />
<Field label="Password" name="password" type="password" required />
<Select
label="Country"
name="country"
options={[
{ value: 'us', label: 'United States' },
{ value: 'ca', label: 'Canada' },
{ value: 'uk', label: 'United Kingdom' }
]}
/>
<button type="submit">Register</button>
</Form>
);
}
2. Layout System
// Flexible layout components
function Stack({ children, spacing = 'medium', direction = 'vertical' }) {
return (
<div className={`stack stack-${direction} stack-${spacing}`}>
{children}
<div>
);
}
function Grid({ children, columns = 3, gap = 'medium' }) {
return (
<div
className={`grid grid-gap-${gap}`}
style={{ gridTemplateColumns: `repeat(${columns}, 1fr)` }}
>
{children}
<div>
);
}
function Container({ children, size = 'medium' }) {
return (
<div className={`container container-${size}`}>
{children}
<div>
);
}
// Compose layouts
function ProductGrid({ products }) {
return (
<Container size="large">
<Stack spacing="large">
<h1>Our Products</h1>
<Grid columns={4} gap="medium">
{products.map(product => (
<ProductCard key={product.id} product={product} />
))}
</Grid>
</Stack>
</Container>
);
}