Component Composition Patterns

Building complex interfaces with reusable components

The Power of Composition

Composition is one of the most powerful concepts in React. It allows you to build complex UIs by combining simpler, reusable components. This approach creates more maintainable, flexible code compared to inheritance-based designs.

graph TD A[Complex UI] --> B[Layout Components] A --> C[Feature Components] A --> D[Utility Components] B --> B1[Header] B --> B2[Sidebar] B --> B3[Content] B --> B4[Footer] C --> C1[UserProfile] C --> C2[ProductList] C --> C3[CheckoutForm] D --> D1[Button] D --> D2[Card] D --> D3[Modal] D --> D4[Form Controls]

Analogy: Building with LEGO vs. Carving a Statue

Think of component composition like building with LEGO blocks, compared to inheritance which is more like carving a statue:

  • Composition (LEGO): You build complex structures by connecting simpler, independent pieces. Each piece has a specific purpose, and you can rearrange them in countless ways.
  • Inheritance (Statue Carving): You start with a big block and chip away to reveal the desired form. Once carved, it's difficult to repurpose parts or significantly change the structure.

React's philosophy of "composition over inheritance" is like preferring LEGO building over statue carving - it creates more flexible, reusable, and maintainable code.

Containment with the Children Prop

The children prop is a special prop that allows components to pass JSX elements to other components. It's one of the most fundamental composition patterns in React.

Basic Children Usage

function Card({ children, title }) {
  return (
    <div className="card">
      {title && <div className="card-header">
        <h2>{title}</h2>
      </div>}
      <div className="card-body">
        {children}
      </div>
    </div>
  );
}

// Usage
function App() {
  return (
    <div className="app">
      <Card title="User Profile">
        <p>Name: John Doe</p>
        <p>Email: john@example.com</p>
        <button>Edit Profile</button>
      </Card>
      
      <Card title="Recent Activity">
        <ul>
          <li>Logged in 2 hours ago</li>
          <li>Updated profile picture</li>
          <li>Posted a new comment</li>
        </ul>
      </Card>
    </div>
  );
}

Multiple Named Content Slots

While children provides a single content slot, sometimes you need multiple slots. You can achieve this using additional props:

function PageLayout({ header, sidebar, main, footer }) {
  return (
    <div className="page-layout">
      <header className="header">
        {header}
      </header>
      
      <div className="content-area">
        <aside className="sidebar">
          {sidebar}
        </aside>
        
        <main className="main-content">
          {main}
        </main>
      </div>
      
      <footer className="footer">
        {footer}
      </footer>
    </div>
  );
}

// Usage
function App() {
  return (
    <PageLayout
      header={<h1>My Application</h1>}
      sidebar={
        <nav>
          <ul>
            <li><a href="/">Home</a></li>
            <li><a href="/about">About</a></li>
            <li><a href="/contact">Contact</a></li>
          </ul>
        </nav>
      }
      main={
        <div>
          <h2>Welcome to My App</h2>
          <p>This is the main content area.</p>
        </div>
      }
      footer={
        <p>© 2025 My Company</p>
      }
    />
  );
}
PageLayout Component header prop sidebar prop main prop footer prop

Real-World Example: Material-UI's Grid System

Libraries like Material-UI use this pattern extensively. For example, their Grid component:

import { Grid, Paper, Typography } from '@material-ui/core';

function Dashboard() {
  return (
    <Grid container spacing={3}>
      <Grid item xs={12}>
        <Paper>
          <Typography variant="h4">Dashboard</Typography>
        </Paper>
      </Grid>
      
      <Grid item xs={12} md={6}>
        <Paper>
          <Typography variant="h6">Recent Sales</Typography>
          <SalesChart />
        </Paper>
      </Grid>
      
      <Grid item xs={12} md={6}>
        <Paper>
          <Typography variant="h6">Customer Activity</Typography>
          <ActivityList />
        </Paper>
      </Grid>
    </Grid>
  );
}

The Grid component uses children to create complex layouts while handling the responsive behavior internally.

Specialization Pattern

Specialization is when you create a component that's a specific version of another component. This pattern is similar to inheritance but implemented through composition.

Basic Specialization

function Dialog({ title, message, children }) {
  return (
    <div className="dialog">
      <div className="dialog-header">
        <h2>{title}</h2>
      </div>
      
      <div className="dialog-body">
        <p>{message}</p>
        {children}
      </div>
    </div>
  );
}

// Specialized version of Dialog
function ConfirmationDialog({ onConfirm, onCancel, ...props }) {
  return (
    <Dialog {...props}>
      <div className="dialog-actions">
        <button onClick={onCancel}>Cancel</button>
        <button onClick={onConfirm} className="primary">Confirm</button>
      </div>
    </Dialog>
  );
}

// Usage
function App() {
  const handleDelete = () => {
    console.log('Item deleted');
  };
  
  const handleCancel = () => {
    console.log('Operation cancelled');
  };
  
  return (
    <div>
      <h1>My App</h1>
      
      <ConfirmationDialog
        title="Delete Item"
        message="Are you sure you want to delete this item? This action cannot be undone."
        onConfirm={handleDelete}
        onCancel={handleCancel}
      />
    </div>
  );
}

Creating a Component Library

The specialization pattern is particularly useful when building a component library with variations:

// Base Button component
function Button({ 
  children, 
  onClick, 
  disabled = false, 
  className = '', 
  type = 'button',
  ...rest 
}) {
  const buttonClass = `button ${className}`.trim();
  
  return (
    <button
      type={type}
      className={buttonClass}
      onClick={onClick}
      disabled={disabled}
      {...rest}
    >
      {children}
    </button>
  );
}

// Specialized Button variants
function PrimaryButton(props) {
  return <Button className="button-primary" {...props} />;
}

function SecondaryButton(props) {
  return <Button className="button-secondary" {...props} />;
}

function DangerButton(props) {
  return <Button className="button-danger" {...props} />;
}

function IconButton({ icon, children, ...props }) {
  return (
    <Button className="button-icon" {...props}>
      {icon}
      {children && <span className="button-text">{children}</span>}
    </Button>
  );
}

// Usage
function App() {
  return (
    <div>
      <h1>Button Examples</h1>
      
      <div className="button-container">
        <PrimaryButton onClick={() => console.log('Primary clicked')}>
          Primary
        </PrimaryButton>
        
        <SecondaryButton onClick={() => console.log('Secondary clicked')}>
          Secondary
        </SecondaryButton>
        
        <DangerButton onClick={() => console.log('Danger clicked')}>
          Delete
        </DangerButton>
        
        <IconButton 
          icon={<span className="icon">🔍</span>} 
          onClick={() => console.log('Search clicked')}
        >
          Search
        </IconButton>
        
        <IconButton 
          icon={<span className="icon">⚙️</span>} 
          onClick={() => console.log('Settings clicked')}
        />
      </div>
    </div>
  );
}

Composition vs. Inheritance

React's documentation recommends composition over inheritance. Here's why:

  • Flexibility: Composition makes it easier to reuse code between components
  • Explicitness: Props and composition make the data flow clear and explicit
  • Isolation: Components are more isolated and focused on specific responsibilities
  • Testability: Composed components are typically easier to test

When you're tempted to use inheritance, try composition first!

Compound Components Pattern

Compound components are a pattern where multiple components work together to form a cohesive UI. This pattern provides a more declarative and flexible API for complex components.

Basic Compound Components

// Accordion component set
function Accordion({ children }) {
  const [activeIndex, setActiveIndex] = useState(null);
  
  // Clone and enhance children with context
  const items = React.Children.map(children, (child, index) => {
    return React.cloneElement(child, {
      isActive: index === activeIndex,
      onActivate: () => setActiveIndex(index === activeIndex ? null : index)
    });
  });
  
  return <div className="accordion">{items}</div>;
}

function AccordionItem({ title, children, isActive, onActivate }) {
  return (
    <div className="accordion-item">
      <div className="accordion-header" onClick={onActivate}>
        <h3>{title}</h3>
        <span className={`accordion-icon ${isActive ? 'active' : ''}`}>
          ▼
        </span>
      </div>
      
      {isActive && (
        <div className="accordion-content">
          {children}
        </div>
      )}
    </div>
  );
}

// Usage
function FAQ() {
  return (
    <div>
      <h2>Frequently Asked Questions</h2>
      
      <Accordion>
        <AccordionItem title="What is React?">
          <p>React is a JavaScript library for building user interfaces.</p>
        </AccordionItem>
        
        <AccordionItem title="How do I install React?">
          <p>You can install React using npm: npm install react react-dom</p>
        </AccordionItem>
        
        <AccordionItem title="What is JSX?">
          <p>JSX is a syntax extension for JavaScript that resembles HTML and makes it easier to write React components.</p>
        </AccordionItem>
      </Accordion>
    </div>
  );
}

Using Context for Compound Components

A more advanced implementation uses React Context to share state between components:

import React, { createContext, useContext, useState } from 'react';

// Create context for the Tabs component
const TabsContext = createContext();

function Tabs({ children, defaultIndex = 0 }) {
  const [activeIndex, setActiveIndex] = useState(defaultIndex);
  
  // Provide context values to children
  return (
    <TabsContext.Provider value={{ activeIndex, setActiveIndex }}>
      <div className="tabs-container">
        {children}
      </div>
    </TabsContext.Provider>
  );
}

function TabList({ children }) {
  return <div className="tab-list">{children}</div>;
}

function Tab({ children, index }) {
  const { activeIndex, setActiveIndex } = useContext(TabsContext);
  const isActive = activeIndex === index;
  
  return (
    <div 
      className={`tab ${isActive ? 'active' : ''}`}
      onClick={() => setActiveIndex(index)}
    >
      {children}
    </div>
  );
}

function TabPanels({ children }) {
  const { activeIndex } = useContext(TabsContext);
  
  // Only render the active panel
  const activePanel = React.Children.toArray(children)[activeIndex];
  
  return <div className="tab-panels">{activePanel}</div>;
}

function TabPanel({ children }) {
  return <div className="tab-panel">{children}</div>;
}

// Expose components as properties of the main component
Tabs.TabList = TabList;
Tabs.Tab = Tab;
Tabs.TabPanels = TabPanels;
Tabs.TabPanel = TabPanel;

// Usage
function ProductInformation() {
  return (
    <div className="product-info">
      <h2>Product XYZ</h2>
      
      <Tabs>
        <Tabs.TabList>
          <Tabs.Tab index={0}>Description</Tabs.Tab>
          <Tabs.Tab index={1}>Specifications</Tabs.Tab>
          <Tabs.Tab index={2}>Reviews</Tabs.Tab>
        </Tabs.TabList>
        
        <Tabs.TabPanels>
          <Tabs.TabPanel>
            <p>This is an amazing product that will change your life.</p>
          </Tabs.TabPanel>
          
          <Tabs.TabPanel>
            <ul>
              <li>Dimensions: 10" x 5" x 2"</li>
              <li>Weight: 2 lbs</li>
              <li>Material: Aluminum</li>
            </ul>
          </Tabs.TabPanel>
          
          <Tabs.TabPanel>
            <p>No reviews yet.</p>
          </Tabs.TabPanel>
        </Tabs.TabPanels>
      </Tabs>
    </div>
  );
}

Real-World Example: React Bootstrap

Many UI libraries use the compound components pattern. For example, React Bootstrap's Dropdown:

import { Dropdown } from 'react-bootstrap';

function UserMenu() {
  return (
    <Dropdown>
      <Dropdown.Toggle variant="success" id="dropdown-basic">
        User Menu
      </Dropdown.Toggle>

      <Dropdown.Menu>
        <Dropdown.Item href="#/profile">Profile</Dropdown.Item>
        <Dropdown.Item href="#/settings">Settings</Dropdown.Item>
        <Dropdown.Divider />
        <Dropdown.Item href="#/logout">Logout</Dropdown.Item>
      </Dropdown.Menu>
    </Dropdown>
  );
}

This pattern provides a clean, declarative API while maintaining logical grouping of related components.

Render Props Pattern

The render props pattern involves passing a function as a prop that returns a React element. This allows the parent component to control what gets rendered while the child handles the logic.

Basic Render Props

function DataFetcher({ url, render }) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);
  
  useEffect(() => {
    async function fetchData() {
      try {
        setLoading(true);
        const response = await fetch(url);
        if (!response.ok) {
          throw new Error(`HTTP error! status: ${response.status}`);
        }
        const result = await response.json();
        setData(result);
        setError(null);
      } catch (e) {
        setError(e.message);
        setData(null);
      } finally {
        setLoading(false);
      }
    }
    
    fetchData();
  }, [url]);
  
  return render({ data, loading, error });
}

// Usage
function UserList() {
  return (
    <div>
      <h2>User List</h2>
      
      <DataFetcher
        url="https://jsonplaceholder.typicode.com/users"
        render={({ data, loading, error }) => {
          if (loading) return <div>Loading users...</div>;
          if (error) return <div>Error: {error}</div>;
          if (!data) return <div>No users found</div>;
          
          return (
            <ul>
              {data.map(user => (
                <li key={user.id}>
                  {user.name} ({user.email})
                </li>
              ))}
            </ul>
          );
        }}
      />
    </div>
  );
}

// Another usage with different rendering
function UserCount() {
  return (
    <div>
      <h2>User Statistics</h2>
      
      <DataFetcher
        url="https://jsonplaceholder.typicode.com/users"
        render={({ data, loading, error }) => {
          if (loading) return <div>Loading data...</div>;
          if (error) return <div>Error: {error}</div>;
          if (!data) return <div>No data found</div>;
          
          return (
            <div className="stat-card">
              <h3>Total Users</h3>
              <div className="stat-value">{data.length}</div>
            </div>
          );
        }}
      />
    </div>
  );
}

Children as a Function

A common variant of the render props pattern uses the children prop as a function:

function MouseTracker({ children }) {
  const [position, setPosition] = useState({ x: 0, y: 0 });
  
  const handleMouseMove = (e) => {
    setPosition({
      x: e.clientX,
      y: e.clientY
    });
  };
  
  return (
    <div 
      className="mouse-tracker" 
      style={{ height: '300px', border: '1px solid #ccc' }}
      onMouseMove={handleMouseMove}
    >
      {/* Call children as a function with the position data */}
      {children(position)}
    </div>
  );
}

// Usage with children as a function
function App() {
  return (
    <div>
      <h1>Mouse Position Tracker</h1>
      
      <MouseTracker>
        {(position) => (
          <div>
            <p>Current mouse position:</p>
            <p>
              X: {position.x}, Y: {position.y}
            </p>
            
            {/* Render a dot that follows the mouse */}
            <div 
              style={{
                position: 'absolute',
                left: position.x,
                top: position.y,
                width: '10px',
                height: '10px',
                borderRadius: '50%',
                backgroundColor: 'red',
                transform: 'translate(-50%, -50%)',
                pointerEvents: 'none'
              }}
            />
          </div>
        )}
      </MouseTracker>
    </div>
  );
}

Render Props vs. Hooks

With the introduction of React Hooks, many use cases for render props can now be implemented with custom hooks:

// Converting the MouseTracker render prop to a hook
function useMousePosition() {
  const [position, setPosition] = useState({ x: 0, y: 0 });
  
  useEffect(() => {
    const handleMouseMove = (e) => {
      setPosition({
        x: e.clientX,
        y: e.clientY
      });
    };
    
    window.addEventListener('mousemove', handleMouseMove);
    
    return () => {
      window.removeEventListener('mousemove', handleMouseMove);
    };
  }, []);
  
  return position;
}

// Usage with hook
function MouseIndicator() {
  const position = useMousePosition();
  
  return (
    <div>
      <p>Current mouse position:</p>
      <p>
        X: {position.x}, Y: {position.y}
      </p>
    </div>
  );
}

Hooks often provide a cleaner, more reusable solution, but render props are still valuable in certain scenarios, especially for component libraries that need to support a wide range of use cases.

Higher-Order Components (HOCs)

Higher-Order Components are functions that take a component and return a new enhanced component. This pattern was common before hooks but is still useful in certain scenarios.

flowchart LR A[BaseComponent] --> B[withEnhancement\nHigher-Order Component] B --> C[EnhancedComponent] style B fill:#f9f0ff,stroke:#722ed1

Basic HOC Pattern

// HOC that adds loading state
function withLoading(WrappedComponent) {
  // Return a new component
  return function WithLoading({ isLoading, ...props }) {
    // Render loading indicator when isLoading is true
    if (isLoading) {
      return <div className="loading-spinner">Loading...</div>;
    }
    
    // Otherwise, render the wrapped component
    return <WrappedComponent {...props} />;
  };
}

// Basic component
function UserList({ users }) {
  return (
    <ul>
      {users.map(user => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  );
}

// Enhanced component with loading state
const UserListWithLoading = withLoading(UserList);

// Usage
function App() {
  const [users, setUsers] = useState([]);
  const [loading, setLoading] = useState(true);
  
  useEffect(() => {
    fetch('https://jsonplaceholder.typicode.com/users')
      .then(response => response.json())
      .then(data => {
        setUsers(data);
        setLoading(false);
      });
  }, []);
  
  return (
    <div>
      <h1>Users</h1>
      <UserListWithLoading isLoading={loading} users={users} />
    </div>
  );
}

HOC with Additional Props

HOCs can add extra props or behavior to components:

// HOC that adds authentication checking
function withAuth(WrappedComponent) {
  return function WithAuth(props) {
    const { isAuthenticated, user } = useAuth(); // Custom hook for auth state
    
    if (!isAuthenticated) {
      return <Navigate to="/login" />; // Redirect to login
    }
    
    // Pass the user prop along with original props
    return <WrappedComponent {...props} user={user} />;
  };
}

// Dashboard that requires authentication
function Dashboard({ user, ...props }) {
  return (
    <div className="dashboard">
      <h1>Welcome, {user.name}</h1>
      <p>Your role: {user.role}</p>
      
      {/* Dashboard content */}
    </div>
  );
}

// Create authenticated version of Dashboard
const AuthenticatedDashboard = withAuth(Dashboard);

// Usage in routes
function App() {
  return (
    <Routes>
      <Route path="/" element={<Home />} />
      <Route path="/login" element={<Login />} />
      <Route path="/dashboard" element={<AuthenticatedDashboard />} />
    </Routes>
  );
}

Composing Multiple HOCs

HOCs can be composed together to add multiple enhancements:

// HOC for logging props
function withLogger(WrappedComponent) {
  return function WithLogger(props) {
    console.log(`Component ${WrappedComponent.name} rendered with props:`, props);
    return <WrappedComponent {...props} />;
  };
}

// HOC for error boundaries
function withErrorBoundary(WrappedComponent) {
  return class WithErrorBoundary extends React.Component {
    state = { hasError: false, error: null };
    
    static getDerivedStateFromError(error) {
      return { hasError: true, error };
    }
    
    componentDidCatch(error, info) {
      console.error('Error caught by boundary:', error, info);
    }
    
    render() {
      if (this.state.hasError) {
        return (
          <div className="error-boundary">
            <h2>Something went wrong!</h2>
            <p>{this.state.error.toString()}</p>
            <button onClick={() => this.setState({ hasError: false, error: null })}>
              Try again
            </button>
          </div>
        );
      }
      
      return <WrappedComponent {...this.props} />;
    }
  };
}

// Apply multiple HOCs (composition)
const EnhancedComponent = withErrorBoundary(withLogger(withAuth(Dashboard)));

// Alternative using compose function (from a library like Recompose or lodash/fp)
// const EnhancedComponent = compose(
//   withErrorBoundary,
//   withLogger,
//   withAuth
// )(Dashboard);

HOCs vs. Hooks

While custom hooks have largely replaced HOCs in modern React development, HOCs still have their place:

HOCs Advantages Custom Hooks Advantages
Can wrap entire components, including render logic More composable and flexible
Can be applied to class components No component nesting or "wrapper hell"
Can modify props and the component tree Better TypeScript support
Work well with the react-redux connect pattern More straightforward testing

In modern React development, hooks are generally preferred for sharing behavior, but HOCs are still valuable for cross-cutting concerns like error boundaries or authentication.

Component Composition Best Practices

graph TD A[Component Composition\nBest Practices] A --> B[Single Responsibility] A --> C[Composability] A --> D[Reusability] A --> E[Flexibility] B --> B1[Components should do one thing well] C --> C1[Design components to work together] D --> D1[Make components generalized and adaptable] E --> E1[Allow customization through props]

Keep Components Focused

Design for Composition

Example: Configurable Card Component

// Less flexible, prop-heavy approach
function Card({ 
  title, 
  subtitle, 
  content, 
  image, 
  actions, 
  footerText, 
  variant, 
  size,
  // many more configuration props...
}) {
  // Complex conditional rendering based on numerous props
}

// More flexible, composition-based approach
function Card({ children, variant = 'default', size = 'medium' }) {
  return (
    <div className={`card card-${variant} card-${size}`}>
      {children}
    </div>
  );
}

Card.Header = function CardHeader({ children }) {
  return <div className="card-header">{children}</div>;
};

Card.Title = function CardTitle({ children }) {
  return <h3 className="card-title">{children}</h3>;
};

Card.Subtitle = function CardSubtitle({ children }) {
  return <div className="card-subtitle">{children}</div>;
};

Card.Image = function CardImage({ src, alt }) {
  return <img className="card-image" src={src} alt={alt} />;
};

Card.Content = function CardContent({ children }) {
  return <div className="card-content">{children}</div>;
};

Card.Actions = function CardActions({ children }) {
  return <div className="card-actions">{children}</div>;
};

Card.Footer = function CardFooter({ children }) {
  return <div className="card-footer">{children}</div>;
};

// Usage
function ProductCard({ product }) {
  return (
    <Card variant="product" size="large">
      <Card.Image src={product.image} alt={product.name} />
      <Card.Header>
        <Card.Title>{product.name}</Card.Title>
        <Card.Subtitle>${product.price.toFixed(2)}</Card.Subtitle>
      </Card.Header>
      <Card.Content>
        <p>{product.description}</p>
      </Card.Content>
      <Card.Actions>
        <button>Add to Cart</button>
        <button>View Details</button>
      </Card.Actions>
      <Card.Footer>
        <p>{product.stock} in stock</p>
      </Card.Footer>
    </Card>
  );
}

Real-World Example: Ant Design's Form

Many popular UI libraries use composition patterns extensively. For example, Ant Design's Form components:

import { Form, Input, Button, Select, DatePicker } from 'antd';

function RegistrationForm() {
  const [form] = Form.useForm();
  
  const onFinish = (values) => {
    console.log('Registration values:', values);
  };
  
  return (
    <Form
      form={form}
      name="register"
      onFinish={onFinish}
      layout="vertical"
    >
      <Form.Item
        name="name"
        label="Full Name"
        rules={[{ required: true, message: 'Please enter your name' }]}
      >
        <Input placeholder="Enter your full name" />
      </Form.Item>
      
      <Form.Item
        name="email"
        label="Email"
        rules={[
          { required: true, message: 'Please enter your email' },
          { type: 'email', message: 'Please enter a valid email' }
        ]}
      >
        <Input placeholder="Enter your email" />
      </Form.Item>
      
      <Form.Item
        name="role"
        label="Role"
        rules={[{ required: true, message: 'Please select a role' }]}
      >
        <Select placeholder="Select your role">
          <Select.Option value="developer">Developer</Select.Option>
          <Select.Option value="designer">Designer</Select.Option>
          <Select.Option value="manager">Manager</Select.Option>
        </Select>
      </Form.Item>
      
      <Form.Item
        name="startDate"
        label="Start Date"
      >
        <DatePicker />
      </Form.Item>
      
      <Form.Item>
        <Button type="primary" htmlType="submit">
          Register
        </Button>
      </Form.Item>
    </Form>
  );
}

This example shows how composition creates a clean, declarative API while maintaining flexibility and reusability.

Handling Cross-Cutting Concerns

Cross-cutting concerns are aspects of your application that affect multiple components, such as authentication, logging, or error handling. Composition patterns can help manage these concerns elegantly.

Composition for Error Boundaries

class ErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false, error: null };
  }
  
  static getDerivedStateFromError(error) {
    return { hasError: true, error };
  }
  
  componentDidCatch(error, errorInfo) {
    // Log the error to an error reporting service
    console.error('Error caught by boundary:', error, errorInfo);
  }
  
  render() {
    if (this.state.hasError) {
      // You can render any custom fallback UI
      return this.props.fallback || (
        <div className="error-boundary">
          <h2>Something went wrong.</h2>
          <p>{this.state.error.toString()}</p>
          <button 
            onClick={() => this.setState({ hasError: false, error: null })}
          >
            Try again
          </button>
        </div>
      );
    }
    
    return this.props.children;
  }
}

// Usage
function App() {
  return (
    <div className="app">
      <Header />
      
      <main>
        <ErrorBoundary fallback={<p>Dashboard failed to load</p>}>
          <Dashboard />
        </ErrorBoundary>
        
        <ErrorBoundary fallback={<p>User stats failed to load</p>}>
          <UserStats />
        </ErrorBoundary>
      </main>
      
      <Footer />
    </div>
  );
}

Context for Authentication

// Create an authentication context
const AuthContext = createContext();

// Authentication provider component
function AuthProvider({ children }) {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);
  
  // Check for existing authentication on mount
  useEffect(() => {
    const checkAuth = async () => {
      try {
        const token = localStorage.getItem('authToken');
        
        if (token) {
          // Validate token with API
          const response = await fetch('/api/auth/validate', {
            headers: {
              'Authorization': `Bearer ${token}`
            }
          });
          
          if (response.ok) {
            const userData = await response.json();
            setUser(userData);
          } else {
            // Invalid token
            localStorage.removeItem('authToken');
            setUser(null);
          }
        }
      } catch (error) {
        console.error('Auth check failed:', error);
        setUser(null);
      } finally {
        setLoading(false);
      }
    };
    
    checkAuth();
  }, []);
  
  // Login function
  const login = async (email, password) => {
    try {
      setLoading(true);
      
      const response = await fetch('/api/auth/login', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json'
        },
        body: JSON.stringify({ email, password })
      });
      
      if (!response.ok) {
        throw new Error('Login failed');
      }
      
      const { user, token } = await response.json();
      
      // Store token
      localStorage.setItem('authToken', token);
      
      // Update state
      setUser(user);
      return true;
    } catch (error) {
      console.error('Login failed:', error);
      return false;
    } finally {
      setLoading(false);
    }
  };
  
  // Logout function
  const logout = () => {
    localStorage.removeItem('authToken');
    setUser(null);
  };
  
  const value = {
    user,
    loading,
    isAuthenticated: !!user,
    login,
    logout
  };
  
  return (
    <AuthContext.Provider value={value}>
      {children}
    </AuthContext.Provider>
  );
}

// Custom hook for using the auth context
function useAuth() {
  const context = useContext(AuthContext);
  if (context === undefined) {
    throw new Error('useAuth must be used within an AuthProvider');
  }
  return context;
}

// Protected route component
function ProtectedRoute({ children }) {
  const { isAuthenticated, loading } = useAuth();
  const location = useLocation();
  
  if (loading) {
    return <div>Loading authentication...</div>;
  }
  
  if (!isAuthenticated) {
    // Redirect to login with return path
    return <Navigate to="/login" state={{ from: location }} replace />;
  }
  
  return children;
}

// Usage in application
function App() {
  return (
    <AuthProvider>
      <Router>
        <Routes>
          <Route path="/" element={<HomePage />} />
          <Route path="/login" element={<LoginPage />} />
          
          {/* Protected routes */}
          <Route 
            path="/dashboard" 
            element={
              <ProtectedRoute>
                <Dashboard />
              </ProtectedRoute>
            } 
          />
          
          <Route 
            path="/profile" 
            element={
              <ProtectedRoute>
                <UserProfile />
              </ProtectedRoute>
            } 
          />
        </Routes>
      </Router>
    </AuthProvider>
  );
}

Composition vs. Inheritance for Cross-Cutting Concerns

React favors composition for handling cross-cutting concerns because it:

  • Maintains separation of concerns
  • Keeps components focused on their primary responsibility
  • Allows for flexible and selective application of behaviors
  • Provides better runtime flexibility than static inheritance

This approach enables a cleaner architecture where concerns like authentication, error handling, and logging can be applied declaratively just where needed.

Practice Activities

Activity 1: Building a Compound Component

Objective: Practice creating a compound component system.

Instructions:

  1. Create a Select compound component system with the following parts:
    • Select: Main container component
    • Select.Trigger: Button that opens the dropdown
    • Select.Options: Container for options
    • Select.Option: Individual option item
  2. Implement shared state using React Context
  3. Add functionality for selecting an option and showing/hiding the dropdown

Activity 2: Converting a Component to Use Composition

Objective: Practice refactoring components to use composition for greater flexibility.

Instructions:

  1. Take the following component and refactor it to use composition:
function Alert({ 
  title, 
  message, 
  type = 'info', 
  onClose, 
  showIcon = true, 
  actions = []
}) {
  return (
    <div className={`alert alert-${type}`}>
      {showIcon && <span className={`alert-icon icon-${type}`}>{getIcon(type)}</span>}
      
      <div className="alert-content">
        {title && <h4 className="alert-title">{title}</h4>}
        <p className="alert-message">{message}</p>
      </div>
      
      {actions.length > 0 && (
        <div className="alert-actions">
          {actions.map((action, index) => (
            <button 
              key={index} 
              className={`alert-action ${action.className || ''}`}
              onClick={action.onClick}
            >
              {action.text}
            </button>
          ))}
        </div>
      )}
      
      {onClose && (
        <button className="alert-close" onClick={onClose}>×</button>
      )}
    </div>
  );
}

// Helper function
function getIcon(type) {
  switch(type) {
    case 'success': return '✓';
    case 'warning': return '⚠';
    case 'error': return '✕';
    default: return 'ℹ';
  }
}

Activity 3: Creating a HOC and Hook Alternative

Objective: Compare different composition patterns for the same functionality.

Instructions:

  1. Create a higher-order component called withDarkMode that adds dark mode functionality to a component
  2. Then implement the same functionality as a custom hook called useDarkMode
  3. Finally, create a context provider approach with DarkModeProvider and useDarkMode context hook
  4. Compare the pros and cons of each approach

Resources for Further Learning

Official Documentation:

Community Resources:

UI Libraries with Strong Composition Patterns:

Summary

In the next lecture, we'll explore React's state management and lifecycle features in more depth.