React
07 / 07

Common Patterns

React Common Patterns

Essential React patterns and design principles that help you write clean, maintainable, and scalable code:

Compound Components Pattern

Compound components are components that work together to form a complete UI. They share an implicit state and communicate with each other, providing a flexible and expressive API.

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

// Context for sharing state
const TabsContext = createContext();

// Parent component
function Tabs({ children, defaultValue }) {
  const [activeTab, setActiveTab] = useState(defaultValue);
  
  return (
    <TabsContext.Provider value={{ activeTab, setActiveTab }}>
      <div className="tabs">{children}</div>
    </TabsContext.Provider>
  );
}

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

// Individual tab
function Tab({ value, children }) {
  const { activeTab, setActiveTab } = useContext(TabsContext);
  const isActive = activeTab === value;
  
  return (
    <button
      onClick={() => setActiveTab(value)}
      className={isActive ? 'tab active' : 'tab'}
    >
      {children}
    </button>
  );
}

// Tab panel
function TabPanel({ value, children }) {
  const { activeTab } = useContext(TabsContext);
  
  if (activeTab !== value) return null;
  
  return <div className="tab-panel">{children}</div>;
}

// Usage - expressive and flexible API
function App() {
  return (
    <Tabs defaultValue="account">
      <TabList>
        <Tab value="account">Account</Tab>
        <Tab value="settings">Settings</Tab>
        <Tab value="notifications">Notifications</Tab>
      </TabList>
      
      <TabPanel value="account">
        <AccountSettings />
      </TabPanel>
      <TabPanel value="settings">
        <GeneralSettings />
      </TabPanel>
      <TabPanel value="notifications">
        <NotificationSettings />
      </TabPanel>
    </Tabs>
  );
}

// Export as compound component
Tabs.List = TabList;
Tabs.Tab = Tab;
Tabs.Panel = TabPanel;

Render Props Pattern

Render props is a technique for sharing code between components using a prop whose value is a function. It allows you to pass rendering logic from parent to child.

// Mouse tracker component using render props
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);
}

// Data fetcher with render props
function DataFetcher({ url, render }) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);
  
  useEffect(() => {
    fetch(url)
      .then(res => res.json())
      .then(data => {
        setData(data);
        setLoading(false);
      })
      .catch(err => {
        setError(err);
        setLoading(false);
      });
  }, [url]);
  
  return render({ data, loading, error });
}

// Usage
function App() {
  return (
    <div>
      {/* Mouse position display */}
      <MouseTracker
        render={({ x, y }) => (
          <div>Mouse position: {x}, {y}</div>
        )}
      />
      
      {/* User data display */}
      <DataFetcher
        url="/api/user"
        render={({ data, loading, error }) => {
          if (loading) return <div>Loading...</div>;
          if (error) return <div>Error: {error.message}</div>;
          return <div>User: {data.name}</div>;
        }}
      />
    </div>
  );
}

Higher-Order Components (HOCs)

HOCs are functions that take a component and return a new enhanced component. They are used to reuse component logic across multiple components.

// withAuth HOC - adds authentication logic
function withAuth(WrappedComponent) {
  return function AuthComponent(props) {
    const { user, loading } = useAuth();
    
    if (loading) {
      return <div>Loading...</div>;
    }
    
    if (!user) {
      return <Navigate to="/login" />;
    }
    
    return <WrappedComponent {...props} user={user} />;
  };
}

// withLogging HOC - adds logging
function withLogging(WrappedComponent, componentName) {
  return function LoggingComponent(props) {
    useEffect(() => {
      console.log(`${componentName} mounted`);
      return () => console.log(`${componentName} unmounted`);
    }, []);
    
    return <WrappedComponent {...props} />;
  };
}

// withData HOC - adds data fetching
function withData(WrappedComponent, fetchData) {
  return function DataComponent(props) {
    const [data, setData] = useState(null);
    const [loading, setLoading] = useState(true);
    const [error, setError] = useState(null);
    
    useEffect(() => {
      fetchData(props)
        .then(data => {
          setData(data);
          setLoading(false);
        })
        .catch(err => {
          setError(err);
          setLoading(false);
        });
    }, [props.id]); // Refetch when ID changes
    
    return <WrappedComponent {...props} data={data} loading={loading} error={error} />;
  };
}

// Usage - compose multiple HOCs
const UserProfile = ({ user, data }) => (
  <div>
    <h1>{user.name}</h1>
    <p>{data.bio}</p>
  </div>
);

const EnhancedUserProfile = withAuth(
  withLogging(
    withData(UserProfile, ({ id }) => fetch(`/api/users/${id}`)),
    'UserProfile'
  )
);

Container/Presentational Pattern

Separate components into containers (logic) and presentational (UI) components for better reusability and testability.

// Presentational component - pure UI
function UserCard({ name, email, avatar, onEdit, onDelete }) {
  return (
    <div className="user-card">
      <img src={avatar} alt={name} />
      <h3>{name}</h3>
      <p>{email}</p>
      <div>
        <button onClick={onEdit}>Edit</button>
        <button onClick={onDelete}>Delete</button>
      </div>
    </div>
  );
}

// Container component - handles logic and state
function UserCardContainer({ userId }) {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);
  
  useEffect(() => {
    fetchUser(userId)
      .then(data => {
        setUser(data);
        setLoading(false);
      });
  }, [userId]);
  
  const handleEdit = () => {
    // Edit logic
    console.log('Edit user:', userId);
  };
  
  const handleDelete = async () => {
    await deleteUser(userId);
    // Update state or navigate
  };
  
  if (loading) return <div>Loading...</div>;
  
  return (
    <UserCard
      name={user.name}
      email={user.email}
      avatar={user.avatar}
      onEdit={handleEdit}
      onDelete={handleDelete}
    />
  );
}

Custom Hooks Pattern

Custom hooks extract reusable logic from components. They follow the naming convention of starting with "use" and can call other hooks.

// useToggle - manage boolean state
function useToggle(initialValue = false) {
  const [value, setValue] = useState(initialValue);
  
  const toggle = useCallback(() => setValue(v => !v), []);
  const setTrue = useCallback(() => setValue(true), []);
  const setFalse = useCallback(() => setValue(false), []);
  
  return [value, { toggle, setTrue, setFalse }];
}

// useDebounce - debounce a value
function useDebounce(value, delay) {
  const [debouncedValue, setDebouncedValue] = useState(value);
  
  useEffect(() => {
    const handler = setTimeout(() => {
      setDebouncedValue(value);
    }, delay);
    
    return () => clearTimeout(handler);
  }, [value, delay]);
  
  return debouncedValue;
}

// useMediaQuery - responsive design
function useMediaQuery(query) {
  const [matches, setMatches] = useState(false);
  
  useEffect(() => {
    const media = window.matchMedia(query);
    setMatches(media.matches);
    
    const listener = (e) => setMatches(e.matches);
    media.addEventListener('change', listener);
    
    return () => media.removeEventListener('change', listener);
  }, [query]);
  
  return matches;
}

// Usage
function SearchInput() {
  const [searchTerm, setSearchTerm] = useState('');
  const debouncedSearchTerm = useDebounce(searchTerm, 500);
  const isMobile = useMediaQuery('(max-width: 768px)');
  const [isOpen, { toggle, setFalse }] = useToggle();
  
  useEffect(() => {
    if (debouncedSearchTerm) {
      // Perform search
      console.log('Searching for:', debouncedSearchTerm);
    }
  }, [debouncedSearchTerm]);
  
  return (
    <div>
      <input
        type="text"
        value={searchTerm}
        onChange={(e) => setSearchTerm(e.target.value)}
        placeholder={isMobile ? 'Search...' : 'Search for anything...'}
      />
      <button onClick={toggle}>
        {isOpen ? 'Close Filters' : 'Open Filters'}
      </button>
      {isOpen && <FilterPanel onClose={setFalse} />}
    </div>
  );
}

Provider Pattern

The Provider pattern uses React Context to provide data to deeply nested components without prop drilling.

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

// Create context
const AuthContext = createContext();

// Provider component
export function AuthProvider({ children }) {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);
  
  useEffect(() => {
    // Check if user is logged in
    checkAuth()
      .then(userData => {
        setUser(userData);
        setLoading(false);
      })
      .catch(() => {
        setUser(null);
        setLoading(false);
      });
  }, []);
  
  const login = async (credentials) => {
    const userData = await authenticate(credentials);
    setUser(userData);
    localStorage.setItem('token', userData.token);
  };
  
  const logout = () => {
    setUser(null);
    localStorage.removeItem('token');
  };
  
  const value = {
    user,
    loading,
    login,
    logout,
    isAuthenticated: !!user,
  };
  
  return (
    <AuthContext.Provider value={value}>
      {children}
    </AuthContext.Provider>
  );
}

// Custom hook for consuming context
export function useAuth() {
  const context = useContext(AuthContext);
  
  if (!context) {
    throw new Error('useAuth must be used within AuthProvider');
  }
  
  return context;
}

// Usage in app
function App() {
  return (
    <AuthProvider>
      <Router>
        <Routes>
          <Route path="/" element={<Home />} />
          <Route path="/login" element={<Login />} />
          <Route path="/dashboard" element={<ProtectedRoute><Dashboard /></ProtectedRoute>} />
        </Routes>
      </Router>
    </AuthProvider>
  );
}

// Protected route component
function ProtectedRoute({ children }) {
  const { isAuthenticated, loading } = useAuth();
  
  if (loading) return <div>Loading...</div>;
  
  return isAuthenticated ? children : <Navigate to="/login" />;
}

Controlled vs Uncontrolled Components

Controlled components have their state controlled by React, while uncontrolled components manage their own state internally.

// Controlled component - React controls the state
function ControlledInput() {
  const [value, setValue] = useState('');
  
  return (
    <input
      value={value}
      onChange={(e) => setValue(e.target.value)}
      placeholder="Controlled input"
    />
  );
}

// Uncontrolled component - DOM controls the state
function UncontrolledInput() {
  const inputRef = useRef();
  
  const handleSubmit = () => {
    console.log('Input value:', inputRef.current.value);
  };
  
  return (
    <div>
      <input
        ref={inputRef}
        defaultValue=""
        placeholder="Uncontrolled input"
      />
      <button onClick={handleSubmit}>Submit</button>
    </div>
  );
}

// Hybrid approach - controlled with default value
function HybridInput({ defaultValue, onChange }) {
  const [value, setValue] = useState(defaultValue);
  
  const handleChange = (e) => {
    const newValue = e.target.value;
    setValue(newValue);
    onChange?.(newValue);
  };
  
  return (
    <input
      value={value}
      onChange={handleChange}
      placeholder="Hybrid input"
    />
  );
}

Prop Drilling Solution - Composition

Instead of passing props through many layers, use component composition to pass components directly where they're needed.

// ❌ Prop drilling - passing props through many layers
function App() {
  const user = useUser();
  return <Layout user={user} />;
}

function Layout({ user }) {
  return <Sidebar user={user} />;
}

function Sidebar({ user }) {
  return <UserMenu user={user} />;
}

function UserMenu({ user }) {
  return <div>{user.name}</div>;
}

// ✅ Component composition - pass components as props
function App() {
  const user = useUser();
  
  return (
    <Layout
      sidebar={<Sidebar userMenu={<UserMenu user={user} />} />}
    >
      <MainContent />
    </Layout>
  );
}

function Layout({ sidebar, children }) {
  return (
    <div className="layout">
      <div className="sidebar">{sidebar}</div>
      <div className="main">{children}</div>
    </div>
  );
}

function Sidebar({ userMenu }) {
  return (
    <div>
      <Navigation />
      {userMenu}
    </div>
  );
}

Lazy Loading and Code Splitting

Load components on demand to reduce initial bundle size and improve performance.

import { lazy, Suspense } from 'react';
import { BrowserRouter, Routes, Route } from 'react-router-dom';

// Lazy load route components
const Home = lazy(() => import('./pages/Home'));
const Dashboard = lazy(() => import('./pages/Dashboard'));
const Settings = lazy(() => import('./pages/Settings'));
const Profile = lazy(() => import('./pages/Profile'));

// Loading fallback component
function PageLoader() {
  return (
    <div className="page-loader">
      <div className="spinner" />
      <p>Loading...</p>
    </div>
  );
}

function App() {
  return (
    <BrowserRouter>
      <Suspense fallback={<PageLoader />}>
        <Routes>
          <Route path="/" element={<Home />} />
          <Route path="/dashboard" element={<Dashboard />} />
          <Route path="/settings" element={<Settings />} />
          <Route path="/profile" element={<Profile />} />
        </Routes>
      </Suspense>
    </BrowserRouter>
  );
}

// Lazy load heavy components
const HeavyChart = lazy(() => import('./components/HeavyChart'));

function Analytics() {
  const [showChart, setShowChart] = useState(false);
  
  return (
    <div>
      <button onClick={() => setShowChart(true)}>Show Chart</button>
      
      {showChart && (
        <Suspense fallback={<div>Loading chart...</div>}>
          <HeavyChart data={analyticsData} />
        </Suspense>
      )}
    </div>
  );
}

Error Boundaries

Error boundaries catch JavaScript errors anywhere in their child component tree and display a fallback UI instead of crashing.

import { Component } from 'react';

// Error boundary class component
class ErrorBoundary extends Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false, error: null };
  }
  
  static getDerivedStateFromError(error) {
    return { hasError: true, error };
  }
  
  componentDidCatch(error, errorInfo) {
    console.error('Error caught by boundary:', error, errorInfo);
    // Log to error reporting service
    logErrorToService(error, errorInfo);
  }
  
  render() {
    if (this.state.hasError) {
      return (
        <div className="error-fallback">
          <h1>Something went wrong</h1>
          <p>{this.state.error?.message}</p>
          <button onClick={() => this.setState({ hasError: false, error: null })}>
            Try again
          </button>
        </div>
      );
    }
    
    return this.props.children;
  }
}

// Usage
function App() {
  return (
    <ErrorBoundary>
      <Router>
        <Routes>
          <Route path="/" element={<Home />} />
          <Route
            path="/dashboard"
            element={
              <ErrorBoundary>
                <Dashboard />
              </ErrorBoundary>
            }
          />
        </Routes>
      </Router>
    </ErrorBoundary>
  );
}

Portal Pattern

Portals provide a way to render children into a DOM node that exists outside the DOM hierarchy of the parent component.

import { createPortal } from 'react-dom';

// Modal component using portal
function Modal({ isOpen, onClose, children }) {
  if (!isOpen) return null;
  
  return createPortal(
    <div className="modal-overlay" onClick={onClose}>
      <div className="modal-content" onClick={(e) => e.stopPropagation()}>
        <button className="modal-close" onClick={onClose}>×</button>
        {children}
      </div>
    </div>,
    document.body // Render to body instead of parent
  );
}

// Tooltip using portal
function Tooltip({ children, content, position }) {
  const [isVisible, setIsVisible] = useState(false);
  const [coords, setCoords] = useState({ x: 0, y: 0 });
  const triggerRef = useRef();
  
  const showTooltip = () => {
    const rect = triggerRef.current.getBoundingClientRect();
    setCoords({ x: rect.left, y: rect.bottom + 5 });
    setIsVisible(true);
  };
  
  return (
    <>
      <span
        ref={triggerRef}
        onMouseEnter={showTooltip}
        onMouseLeave={() => setIsVisible(false)}
      >
        {children}
      </span>
      
      {isVisible && createPortal(
        <div
          className="tooltip"
          style={{ position: 'absolute', left: coords.x, top: coords.y }}
        >
          {content}
        </div>,
        document.body
      )}
    </>
  );
}

// Usage
function App() {
  const [showModal, setShowModal] = useState(false);
  
  return (
    <div>
      <button onClick={() => setShowModal(true)}>Open Modal</button>
      
      <Modal isOpen={showModal} onClose={() => setShowModal(false)}>
        <h2>Modal Title</h2>
        <p>Modal content here...</p>
      </Modal>
      
      <Tooltip content="Helpful information">
        <span>Hover me</span>
      </Tooltip>
    </div>
  );
}

Keep your own version of these notes — editable, searchable, and organised by your stack.

Start free