Composition vs Inheritance in React

Building Flexible and Reusable Component Hierarchies

Overview

Welcome to the powerful world of component composition! While traditional object-oriented programming relies heavily on inheritance, React embraces composition as its primary pattern for building UIs. Think of composition like building with LEGO blocks - you create complex structures by combining simple, reusable pieces. Today, you'll discover why React favors composition and how to leverage it effectively.

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.

graph TD subgraph "Inheritance (Traditional OOP)" A[Animal] --> B[Dog] A --> C[Cat] B --> D[Labrador] B --> E[Poodle] end subgraph "Composition (React Way)" F[Button] --> G[IconButton] H[Icon] --> G F --> I[TextButton] J[Text] --> I F --> K[IconTextButton] H --> K J --> K end style A fill:#f99,stroke:#333,stroke-width:2px style F fill:#9cf,stroke:#333,stroke-width:2px

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

graph TD A[Design Decision] --> B{Need Polymorphism?} B -->|Yes| C{UI Components?} B -->|No| D[Use Composition] C -->|Yes| E[Use Composition] C -->|No| F[Consider Inheritance] E --> G[Props] E --> H[Children] E --> I[HOCs] E --> J[Render Props] D --> K[Flexibility] D --> L[Reusability] D --> M[Maintainability] style A fill:#f9f,stroke:#333,stroke-width:2px style E fill:#9cf,stroke:#333,stroke-width:2px style D fill:#9cf,stroke:#333,stroke-width:2px

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>
    );
}
            

Best Practices

graph TD A[Composition Best Practices] --> B[Use Props for Configuration] A --> C[Use Children for Content] A --> D[Keep Components Focused] A --> E[Avoid Prop Drilling] B --> F[Flexible APIs] C --> G[Containment] D --> H[Single Responsibility] E --> I[Context for Deep Data] E --> J[Composition for Structure] style A fill:#f9f,stroke:#333,stroke-width:2px

Practice Exercises

Exercise 1: Build a Card System

Create a flexible card component system:


function CardSystem() {
    // Create components:
    // - Card (container)
    // - CardHeader
    // - CardBody
    // - CardFooter
    // - CardImage
    // Allow flexible composition of these parts
}
            

Exercise 2: Modal System

Build a reusable modal system with composition:


function ModalSystem() {
    // Create components:
    // - Modal (container)
    // - ModalHeader
    // - ModalBody
    // - ModalFooter
    // - ModalTrigger
    // Handle open/close state
}
            

Exercise 3: Navigation Component

Create a flexible navigation system:


function NavigationSystem() {
    // Create components:
    // - Nav
    // - NavItem
    // - NavDropdown
    // - NavGroup
    // Support nested navigation
}
            

Key Takeaways

What's Next?

In our next lesson, we'll explore the Children props in detail and learn advanced techniques for working with component children!

Homework