FrontendReactJavaScriptPerformanceHooksPatterns

Modern React Patterns: Hooks, Context, and Performance

Advanced patterns for building maintainable and performant React applications

Ahmed Attafi
January 10, 2025
12 min read

React has evolved significantly since its introduction, and with it, the patterns and practices for building scalable applications have matured. Modern React development leverages hooks, context, and advanced patterns to create maintainable, performant applications that scale with your team and user base.

App
Auth Context
Theme Context
Data Context

Custom Hooks

Components

Memoization

Error Boundaries

Optimized
Memoized
Reusable
Modern React Architecture

1. Custom Hooks for Reusable Logic

Custom hooks are the foundation of modern React patterns. They allow you to extract component logic into reusable functions, making your codebase more maintainable and testable.

useApi Hook Pattern

// useApi.ts - Reusable API hook
import { useState, useEffect, useCallback } from 'react';

interface ApiState<T> {
  data: T | null;
  loading: boolean;
  error: Error | null;
}

export function useApi<T>(url: string, options?: RequestInit) {
  const [state, setState] = useState<ApiState<T>>({
    data: null,
    loading: true,
    error: null
  });

  const fetchData = useCallback(async () => {
    try {
      setState(prev => ({ ...prev, loading: true, error: null }));
      const response = await fetch(url, options);
      
      if (!response.ok) {
        throw new Error(`HTTP error! status: ${response.status}`);
      }
      
      const data = await response.json();
      setState({ data, loading: false, error: null });
    } catch (error) {
      setState({
        data: null,
        loading: false,
        error: error instanceof Error ? error : new Error('Unknown error')
      });
    }
  }, [url, options]);

  useEffect(() => {
    fetchData();
  }, [fetchData]);

  return { ...state, refetch: fetchData };
}

// Usage in component
function UserProfile({ userId }: { userId: string }) {
  const { data: user, loading, error, refetch } = useApi<User>(`/api/users/${userId}`);

  if (loading) return <LoadingSpinner />;
  if (error) return <ErrorMessage error={error} onRetry={refetch} />;
  if (!user) return <NotFound />;

  return <UserCard user={user} />;
}

useLocalStorage Hook

// useLocalStorage.ts - Persistent state hook
import { useState, useEffect } from 'react';

export function useLocalStorage<T>(key: string, initialValue: T) {
  // Get from local storage then parse stored json or return initialValue
  const [storedValue, setStoredValue] = useState<T>(() => {
    try {
      const item = window.localStorage.getItem(key);
      return item ? JSON.parse(item) : initialValue;
    } catch (error) {
      console.warn(`Error reading localStorage key "${key}":`, error);
      return initialValue;
    }
  });

  // Return a wrapped version of useState's setter function that persists the new value to localStorage
  const setValue = (value: T | ((val: T) => T)) => {
    try {
      const valueToStore = value instanceof Function ? value(storedValue) : value;
      setStoredValue(valueToStore);
      window.localStorage.setItem(key, JSON.stringify(valueToStore));
    } catch (error) {
      console.warn(`Error setting localStorage key "${key}":`, error);
    }
  };

  return [storedValue, setValue] as const;
}

// Usage
function ThemeProvider({ children }: { children: React.ReactNode }) {
  const [theme, setTheme] = useLocalStorage('theme', 'light');
  
  return (
    <ThemeContext.Provider value={{ theme, setTheme }}>
      {children}
    </ThemeContext.Provider>
  );
}

2. Advanced Context Patterns

Context is powerful for sharing state across your component tree, but it needs to be used wisely to avoid performance issues and maintain clean architecture.

Split Context Pattern

Split your context into separate contexts for state and actions to optimize re-renders.

// Split context pattern for better performance
interface UserState {
  user: User | null;
  loading: boolean;
  error: string | null;
}

interface UserActions {
  login: (credentials: Credentials) => Promise<void>;
  logout: () => void;
  updateProfile: (data: Partial<User>) => Promise<void>;
}

// Separate contexts
const UserStateContext = createContext<UserState | undefined>(undefined);
const UserActionsContext = createContext<UserActions | undefined>(undefined);

// Provider component
export function UserProvider({ children }: { children: React.ReactNode }) {
  const [state, setState] = useState<UserState>({
    user: null,
    loading: false,
    error: null
  });

  const actions: UserActions = useMemo(() => ({
    login: async (credentials) => {
      setState(prev => ({ ...prev, loading: true, error: null }));
      try {
        const user = await authService.login(credentials);
        setState({ user, loading: false, error: null });
      } catch (error) {
        setState(prev => ({ 
          ...prev, 
          loading: false, 
          error: error.message 
        }));
      }
    },
    logout: () => {
      authService.logout();
      setState({ user: null, loading: false, error: null });
    },
    updateProfile: async (data) => {
      // Implementation
    }
  }), []);

  return (
    <UserStateContext.Provider value={state}>
      <UserActionsContext.Provider value={actions}>
        {children}
      </UserActionsContext.Provider>
    </UserStateContext.Provider>
  );
}

// Custom hooks for consuming context
export function useUserState() {
  const context = useContext(UserStateContext);
  if (!context) {
    throw new Error('useUserState must be used within UserProvider');
  }
  return context;
}

export function useUserActions() {
  const context = useContext(UserActionsContext);
  if (!context) {
    throw new Error('useUserActions must be used within UserProvider');
  }
  return context;
}

3. Performance Optimization Techniques

Performance optimization in React involves preventing unnecessary re-renders, optimizing expensive computations, and managing component lifecycle efficiently.

React.memo

Prevent re-renders when props haven't changed

const ExpensiveComponent = React.memo(
  ({ data, onUpdate }) => {
    return <ComplexVisualization data={data} />;
  },
  (prevProps, nextProps) => {
    // Custom comparison
    return prevProps.data.id === nextProps.data.id;
  }
);

useMemo & useCallback

Memoize expensive calculations and functions

const processedData = useMemo(() => {
  return expensiveDataProcessing(rawData);
}, [rawData]);

const handleClick = useCallback((id: string) => {
  onItemClick(id);
}, [onItemClick]);

Virtual Scrolling Pattern

// useVirtualScrolling.ts - Custom hook for large lists
import { useState, useEffect, useMemo } from 'react';

interface VirtualScrollOptions {
  itemHeight: number;
  containerHeight: number;
  overscan?: number;
}

export function useVirtualScrolling<T>(
  items: T[],
  options: VirtualScrollOptions
) {
  const [scrollTop, setScrollTop] = useState(0);
  const { itemHeight, containerHeight, overscan = 5 } = options;

  const visibleRange = useMemo(() => {
    const startIndex = Math.max(0, Math.floor(scrollTop / itemHeight) - overscan);
    const endIndex = Math.min(
      items.length - 1,
      Math.ceil((scrollTop + containerHeight) / itemHeight) + overscan
    );
    return { startIndex, endIndex };
  }, [scrollTop, itemHeight, containerHeight, overscan, items.length]);

  const visibleItems = useMemo(() => {
    return items.slice(visibleRange.startIndex, visibleRange.endIndex + 1)
      .map((item, index) => ({
        item,
        index: visibleRange.startIndex + index
      }));
  }, [items, visibleRange]);

  const totalHeight = items.length * itemHeight;
  const offsetY = visibleRange.startIndex * itemHeight;

  return {
    visibleItems,
    totalHeight,
    offsetY,
    onScroll: (e: React.UIEvent<HTMLElement>) => {
      setScrollTop(e.currentTarget.scrollTop);
    }
  };
}

// Usage in component
function LargeList({ items }: { items: Item[] }) {
  const { visibleItems, totalHeight, offsetY, onScroll } = useVirtualScrolling(
    items,
    { itemHeight: 50, containerHeight: 400 }
  );

  return (
    <div className="h-96 overflow-auto" onScroll={onScroll}>
      <div style={{ height: totalHeight, position: 'relative' }}>
        <div style={{ transform: `translateY(${offsetY}px)` }}>
          {visibleItems.map(({ item, index }) => (
            <div key={index} className="h-12 border-b">
              <ItemComponent item={item} />
            </div>
          ))}
        </div>
      </div>
    </div>
  );
}

4. Error Handling and Boundaries

Robust error handling is crucial for production applications. React Error Boundaries provide a way to catch JavaScript errors anywhere in the component tree.

Enhanced Error Boundary with Hooks

// ErrorBoundary.tsx - Modern error boundary implementation
import React, { Component, ErrorInfo, ReactNode } from 'react';

interface Props {
  children: ReactNode;
  fallback?: ReactNode;
  onError?: (error: Error, errorInfo: ErrorInfo) => void;
}

interface State {
  hasError: boolean;
  error?: Error;
}

export class ErrorBoundary extends Component<Props, State> {
  constructor(props: Props) {
    super(props);
    this.state = { hasError: false };
  }

  static getDerivedStateFromError(error: Error): State {
    return { hasError: true, error };
  }

  componentDidCatch(error: Error, errorInfo: ErrorInfo) {
    console.error('Error caught by boundary:', error, errorInfo);
    this.props.onError?.(error, errorInfo);
    
    // Log to error reporting service
    if (process.env.NODE_ENV === 'production') {
      // errorReportingService.logError(error, errorInfo);
    }
  }

  render() {
    if (this.state.hasError) {
      return this.props.fallback || (
        <div className="p-6 bg-red-50 border border-red-200 rounded-lg">
          <h2 className="text-lg font-semibold text-red-800 mb-2">
            Something went wrong
          </h2>
          <p className="text-red-600 mb-4">
            We're sorry, but something unexpected happened.
          </p>
          <button
            onClick={() => this.setState({ hasError: false })}
            className="px-4 py-2 bg-red-600 text-white rounded hover:bg-red-700"
          >
            Try again
          </button>
        </div>
      );
    }

    return this.props.children;
  }
}

// Hook for error boundary in functional components
export function useErrorHandler() {
  const [error, setError] = useState<Error | null>(null);

  const resetError = () => setError(null);
  
  const captureError = (error: Error) => {
    setError(error);
  };

  if (error) {
    throw error; // This will be caught by the nearest Error Boundary
  }

  return { captureError, resetError };
}

5. Testing Modern React Components

Testing React components with hooks and context requires specific patterns and tools. Here's how to test modern React applications effectively.

Testing Custom Hooks

// useApi.test.ts
import { renderHook, waitFor } from '@testing-library/react';
import { useApi } from './useApi';

// Mock fetch
global.fetch = jest.fn();

describe('useApi', () => {
  beforeEach(() => {
    (fetch as jest.Mock).mockClear();
  });

  it('should fetch data successfully', async () => {
    const mockData = { id: 1, name: 'Test User' };
    (fetch as jest.Mock).mockResolvedValueOnce({
      ok: true,
      json: async () => mockData
    });

    const { result } = renderHook(() => useApi('/api/users/1'));

    expect(result.current.loading).toBe(true);
    expect(result.current.data).toBe(null);

    await waitFor(() => {
      expect(result.current.loading).toBe(false);
    });

    expect(result.current.data).toEqual(mockData);
    expect(result.current.error).toBe(null);
  });

  it('should handle errors properly', async () => {
    (fetch as jest.Mock).mockRejectedValueOnce(new Error('Network error'));

    const { result } = renderHook(() => useApi('/api/users/1'));

    await waitFor(() => {
      expect(result.current.loading).toBe(false);
    });

    expect(result.current.error).toBeTruthy();
    expect(result.current.data).toBe(null);
  });
});

6. Production-Ready Best Practices

Code Splitting

  • • Use React.lazy() for route-level splitting
  • • Implement component-level lazy loading
  • • Optimize bundle sizes with webpack analysis
  • • Use dynamic imports for large libraries

State Management

  • • Keep state as close to where it's used
  • • Use context sparingly for global state
  • • Consider state machines for complex flows
  • • Implement optimistic updates

Accessibility

  • • Use semantic HTML elements
  • • Implement keyboard navigation
  • • Add ARIA labels and roles
  • • Test with screen readers

Monitoring

  • • Implement error tracking
  • • Monitor Core Web Vitals
  • • Track user interactions
  • • Set up performance budgets

Conclusion

Modern React development is about more than just writing components. It's about creating maintainable, performant, and accessible applications that scale with your team and user base.

By implementing these patterns and best practices, you'll build React applications that are not only functional but also robust, testable, and ready for production. Remember that these patterns should be applied judiciously – not every component needs to be memoized, and not every piece of state needs to be in context.

Ready to level up your React skills?

Start implementing these patterns in your next React project.

Enjoyed this deep dive into React patterns?

Share it with your development team and help spread modern React knowledge.