Context API and Global State Management

1. createContext and Provider Component Patterns

API Syntax Description Return Value
createContext createContext(defaultValue) Creates a Context object with default value Context object with Provider and Consumer
Context.Provider <MyContext.Provider value={value}> Provides context value to child components Accepts value prop, passes to consumers
Context.Consumer <MyContext.Consumer>{value => ...}</MyContext.Consumer> Render prop pattern for consuming context Legacy - use useContext hook instead
Provider Pattern Implementation Use Case
Simple Provider <Context.Provider value={state}>{children}</Context.Provider> Pass static or computed value directly
Provider Component RECOMMENDED Wrap Provider in custom component with state management Encapsulate state logic, cleaner API for consumers
Provider with useState Provider component manages state with useState hook Simple global state, few update operations
Provider with useReducer Provider component manages state with useReducer Complex state logic, many actions, Redux-like

Example: Basic Context creation and Provider setup

import { createContext, useState } from 'react';

// 1. Create Context with default value
const ThemeContext = createContext('light');

// 2. Create Provider component
function ThemeProvider({ children }) {
  const [theme, setTheme] = useState('light');
  
  const toggleTheme = () => {
    setTheme(prev => prev === 'light' ? 'dark' : 'light');
  };
  
  // Value object with state and updaters
  const value = { theme, toggleTheme };
  
  return (
    <ThemeContext.Provider value={value}>
      {children}
    </ThemeContext.Provider>
  );
}

// 3. Usage: Wrap app with Provider
function App() {
  return (
    <ThemeProvider>
      <Header />
      <Main />
      <Footer />
    </ThemeProvider>
  );
}

Example: Provider with useReducer for complex state

import { createContext, useReducer } from 'react';

// Context
const CartContext = createContext(null);

// Reducer
function cartReducer(state, action) {
  switch (action.type) {
    case 'ADD_ITEM':
      return { ...state, items: [...state.items, action.payload] };
    case 'REMOVE_ITEM':
      return { ...state, items: state.items.filter(i => i.id !== action.payload) };
    case 'UPDATE_QUANTITY':
      return {
        ...state,
        items: state.items.map(i =>
          i.id === action.payload.id ? { ...i, quantity: action.payload.quantity } : i
        )
      };
    case 'CLEAR':
      return { ...state, items: [] };
    default:
      return state;
  }
}

// Provider component
function CartProvider({ children }) {
  const [state, dispatch] = useReducer(cartReducer, { items: [], total: 0 });
  
  // Derived values
  const itemCount = state.items.reduce((sum, item) => sum + item.quantity, 0);
  const totalPrice = state.items.reduce((sum, item) => sum + item.price * item.quantity, 0);
  
  const value = {
    items: state.items,
    itemCount,
    totalPrice,
    dispatch
  };
  
  return (
    <CartContext.Provider value={value}>
      {children}
    </CartContext.Provider>
  );
}
Default Value: The default value in createContext(defaultValue) is only used when a component doesn't have a matching Provider above it in the tree. It's useful for testing components in isolation.

2. useContext Hook and Context Consumption

API Syntax Description Return Value
useContext useContext(MyContext) Subscribes component to context changes Current context value from nearest Provider
Context Argument Pass Context object (not Provider/Consumer) Must be the Context object from createContext Returns value prop from nearest Provider
Consumption Pattern Implementation Benefit
Direct useContext const value = useContext(MyContext) Simple, direct access to context
Custom Hook RECOMMENDED const useMyContext = () => useContext(MyContext) Encapsulation, validation, better API
Selective Access const { user, setUser } = useContext(UserContext) Destructure only needed values from context

Example: Consuming context with useContext

import { useContext } from 'react';

// Consume context in component
function ThemeToggle() {
  const { theme, toggleTheme } = useContext(ThemeContext);
  
  return (
    <button onClick={toggleTheme}>
      Current theme: {theme}
    </button>
  );
}

// Another consumer
function ThemedPanel() {
  const { theme } = useContext(ThemeContext);
  
  const styles = {
    background: theme === 'light' ? '#fff' : '#333',
    color: theme === 'light' ? '#333' : '#fff'
  };
  
  return <div style={styles}>Themed content</div>;
}

Example: Custom hook for context with validation

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

const AuthContext = createContext(null);

// Provider
export function AuthProvider({ children }) {
  const [user, setUser] = useState(null);
  const [isAuthenticated, setIsAuthenticated] = useState(false);
  
  const login = (credentials) => {
    // Login logic
    setUser({ name: credentials.username });
    setIsAuthenticated(true);
  };
  
  const logout = () => {
    setUser(null);
    setIsAuthenticated(false);
  };
  
  const value = { user, isAuthenticated, login, logout };
  
  return (
    <AuthContext.Provider value={value}>
      {children}
    </AuthContext.Provider>
  );
}

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

// Usage in component
function UserProfile() {
  const { user, logout } = useAuth(); // Safe, validated access
  
  return (
    <div>
      <p>Welcome, {user.name}</p>
      <button onClick={logout}>Logout</button>
    </div>
  );
}
Warning: Components using useContext will re-render when the context value changes. This happens even if the component only uses a small part of the context value. See section 3.4 for optimization techniques.

3. Multiple Context Providers and Context Composition

Pattern Implementation Use Case
Nested Providers Stack multiple Providers in tree hierarchy Different concerns (theme, auth, settings, etc.)
Composed Provider Single component wrapping multiple Providers Reduce nesting, cleaner App component
Dependent Contexts One context consumes another context's value Context depends on data from another context
Composition Strategy Code Pattern Pros/Cons
Manual Nesting <A><B><C>{children}</C></B></A> ✅ Explicit, clear dependencies
❌ Verbose, deep nesting
Provider Composer <ComposeProviders providers={[A, B, C]}> ✅ Clean, scalable
❌ Extra abstraction
Single Root Provider <AppProvider> wraps all contexts ✅ Single import, organized
❌ All contexts loaded together

Example: Multiple providers composition

// Manual nesting (simple but verbose)
function App() {
  return (
    <ThemeProvider>
      <AuthProvider>
        <LanguageProvider>
          <NotificationProvider>
            <Routes />
          </NotificationProvider>
        </LanguageProvider>
      </AuthProvider>
    </ThemeProvider>
  );
}

// Better: Compose providers helper
function ComposeProviders({ providers, children }) {
  return providers.reduceRight((acc, Provider) => {
    return <Provider>{acc}</Provider>;
  }, children);
}

function App() {
  return (
    <ComposeProviders providers={[
      ThemeProvider,
      AuthProvider,
      LanguageProvider,
      NotificationProvider
    ]}>
      <Routes />
    </ComposeProviders>
  );
}

// Best: Single root provider
function AppProvider({ children }) {
  return (
    <ThemeProvider>
      <AuthProvider>
        <LanguageProvider>
          <NotificationProvider>
            {children}
          </NotificationProvider>
        </LanguageProvider>
      </AuthProvider>
    </ThemeProvider>
  );
}

function App() {
  return (
    <AppProvider>
      <Routes />
    </AppProvider>
  );
}

Example: Dependent contexts

// UserPreferences context depends on AuthContext
function UserPreferencesProvider({ children }) {
  const { user } = useAuth(); // Depends on AuthContext
  const [preferences, setPreferences] = useState(null);
  
  useEffect(() => {
    if (user) {
      // Load user preferences when user is available
      loadPreferences(user.id).then(setPreferences);
    } else {
      setPreferences(null);
    }
  }, [user]);
  
  const value = { preferences, setPreferences };
  
  return (
    <PreferencesContext.Provider value={value}>
      {children}
    </PreferencesContext.Provider>
  );
}

// Must be nested under AuthProvider
function App() {
  return (
    <AuthProvider>
      <UserPreferencesProvider>
        <Dashboard />
      </UserPreferencesProvider>
    </AuthProvider>
  );
}

4. Context Performance Optimization Techniques

Problem Cause Solution
Unnecessary Re-renders Context value changes, all consumers re-render Split contexts, memoize value, use selectors
New Object Every Render Provider creates new value object on each render Memoize context value with useMemo
Consuming Entire Context Component re-renders even if it uses one field Split context by concern, use selector pattern
Optimization Technique Implementation Benefit
Memoize Context Value ESSENTIAL const value = useMemo(() => ({...}), [deps]) Prevent re-renders from object identity changes
Split Context by Concern Separate contexts for data and updaters Components only re-render when needed data changes
React.memo on Consumers export default React.memo(Component) Prevent re-renders from parent updates
Selector Pattern Custom hook with selector function Subscribe to specific slice of context state
Separate State/Dispatch Contexts One context for state, another for dispatch Components using only dispatch never re-render

Example: Memoizing context value

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

// ❌ Bad: New object every render, causes unnecessary re-renders
function BadProvider({ children }) {
  const [count, setCount] = useState(0);
  const [name, setName] = useState('');
  
  // New object reference on every render!
  const value = { count, setCount, name, setName };
  
  return <MyContext.Provider value={value}>{children}</MyContext.Provider>;
}

// ✅ Good: Memoized value, only changes when dependencies change
function GoodProvider({ children }) {
  const [count, setCount] = useState(0);
  const [name, setName] = useState('');
  
  // Memoized - same object reference unless count or name changes
  const value = useMemo(
    () => ({ count, setCount, name, setName }),
    [count, name]
  );
  
  return <MyContext.Provider value={value}>{children}</MyContext.Provider>;
}

Example: Split context for state and dispatch

import { createContext, useContext, useReducer, useMemo } from 'react';

// Separate contexts for state and dispatch
const TodoStateContext = createContext(null);
const TodoDispatchContext = createContext(null);

function TodoProvider({ children }) {
  const [state, dispatch] = useReducer(todoReducer, initialState);
  
  // State context value - changes when state changes
  const stateValue = useMemo(() => state, [state]);
  
  // Dispatch context value - never changes (dispatch is stable)
  const dispatchValue = useMemo(() => dispatch, []);
  
  return (
    <TodoStateContext.Provider value={stateValue}>
      <TodoDispatchContext.Provider value={dispatchValue}>
        {children}
      </TodoDispatchContext.Provider>
    </TodoStateContext.Provider>
  );
}

// Hooks for consuming
export function useTodoState() {
  const context = useContext(TodoStateContext);
  if (!context) throw new Error('Must be used within TodoProvider');
  return context;
}

export function useTodoDispatch() {
  const context = useContext(TodoDispatchContext);
  if (!context) throw new Error('Must be used within TodoProvider');
  return context;
}

// Usage: Components only using dispatch never re-render when state changes
function AddTodoButton() {
  const dispatch = useTodoDispatch(); // Won't re-render on state changes
  
  return (
    <button onClick={() => dispatch({ type: 'ADD', payload: 'New Todo' })}>
      Add Todo
    </button>
  );
}

// Component using state re-renders when state changes
function TodoList() {
  const { todos } = useTodoState(); // Re-renders when state changes
  return <ul>{todos.map(t => <li key={t.id}>{t.text}</li>)}</ul>;
}

Example: Selector pattern for granular subscriptions

// Custom hook with selector
function useStoreSelector(selector) {
  const store = useContext(StoreContext);
  return selector(store);
}

// Usage: Subscribe to specific data
function UserProfile() {
  // Only re-renders when user changes, not when cart or settings change
  const user = useStoreSelector(store => store.user);
  
  return <div>{user.name}</div>;
}

function CartCount() {
  // Only re-renders when cart items change
  const itemCount = useStoreSelector(store => store.cart.items.length);
  
  return <span>{itemCount} items</span>;
}
Performance Tip: For high-frequency updates, consider external state management libraries like Zustand or Jotai that have built-in selector optimizations, or use useSyncExternalStore for external stores.

5. Context vs Prop Drilling Trade-offs

Aspect Prop Drilling Context API
Explicitness ✅ Explicit data flow, easy to trace ❌ Implicit, harder to track dependencies
Boilerplate ❌ Pass props through many components ✅ Minimal, direct access anywhere
Reusability ✅ Components are portable, self-contained ❌ Components coupled to context structure
Performance ✅ Optimal, only affected components re-render ⚠️ All consumers re-render on context change
Testing ✅ Easy to test, pass props directly ❌ Must wrap in Provider, mock context
Refactoring ❌ Difficult to change prop names/structure ✅ Change context, consumers auto-update
Use Prop Drilling When Use Context When
2-3 levels of component nesting 4+ levels of component nesting
Props change frequently Props change infrequently (theme, locale, auth)
Component reusability is priority Developer experience is priority
Data is specific to component tree branch Data is truly global (app-wide)
Simple, small applications Medium to large applications
Performance is critical Convenience outweighs performance concerns

Example: When prop drilling is acceptable

// Acceptable: 2-3 levels, clear data flow
function App() {
  const [user, setUser] = useState(null);
  return <Dashboard user={user} setUser={setUser} />;
}

function Dashboard({ user, setUser }) {
  return <UserProfile user={user} setUser={setUser} />;
}

function UserProfile({ user, setUser }) {
  return <div>{user.name}</div>;
}

// Problematic: 5+ levels, props only used at the end
function App() {
  const [theme, setTheme] = useState('light');
  return <Layout theme={theme} setTheme={setTheme} />;
}

function Layout({ theme, setTheme }) {
  return <Sidebar theme={theme} setTheme={setTheme} />;
}

function Sidebar({ theme, setTheme }) {
  return <Menu theme={theme} setTheme={setTheme} />;
}

function Menu({ theme, setTheme }) {
  return <MenuItem theme={theme} setTheme={setTheme} />;
}

function MenuItem({ theme, setTheme }) {
  // Finally used here, but passed through 4 intermediate components
  return <button style={{ background: theme }}>Item</button>;
}

Decision Framework

  • Start with props - simpler, more explicit
  • Move to Context when props pass through 4+ components
  • Use Context for: theme, locale, auth, global UI state
  • Avoid Context for: frequently changing data, component-specific state
  • Hybrid approach: Context for global, props for local tree data

6. Context Testing and Development Patterns

Testing Pattern Implementation Use Case
Wrap in Provider Use real Provider with test values Integration tests, test Provider + Consumer
Custom Render Wrapper Create test utility wrapping component in Providers Avoid repeating Provider setup in every test
Mock Context Value Create mock Provider with controlled values Test component with specific context states
Test Provider Logic Test Provider component's state management separately Unit test context logic without consumers
Development Pattern Description Benefit
Context DevTools Use React DevTools to inspect context values Debug context state and changes
Context Logger Add logging to Provider for state changes Track when and why context updates
TypeScript Inference Strongly type context for autocomplete and safety Catch errors at compile time, better DX
Default Value Warning Throw error in custom hook if context is null Catch missing Provider early in development

Example: Testing components with Context

import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';

// Test with real Provider
test('ThemeToggle changes theme', async () => {
  render(
    <ThemeProvider>
      <ThemeToggle />
    </ThemeProvider>
  );
  
  const button = screen.getByRole('button');
  expect(button).toHaveTextContent('light');
  
  await userEvent.click(button);
  expect(button).toHaveTextContent('dark');
});

// Custom render wrapper for multiple providers
function renderWithProviders(ui, { themeValue = 'light', ...options } = {}) {
  function Wrapper({ children }) {
    return (
      <ThemeProvider initialTheme={themeValue}>
        <AuthProvider>
          <LanguageProvider>
            {children}
          </LanguageProvider>
        </AuthProvider>
      </ThemeProvider>
    );
  }
  
  return render(ui, { wrapper: Wrapper, ...options });
}

// Usage with custom wrapper
test('component renders with dark theme', () => {
  renderWithProviders(<MyComponent />, { themeValue: 'dark' });
  // Test component with dark theme
});

Example: Mock context for testing

// Mock Provider for testing
function MockAuthProvider({ children, value }) {
  return (
    <AuthContext.Provider value={value}>
      {children}
    </AuthContext.Provider>
  );
}

test('shows logout button when authenticated', () => {
  const mockValue = {
    user: { name: 'John', id: 1 },
    isAuthenticated: true,
    logout: jest.fn()
  };
  
  render(
    <MockAuthProvider value={mockValue}>
      <UserMenu />
    </MockAuthProvider>
  );
  
  expect(screen.getByText('Logout')).toBeInTheDocument();
});

test('shows login button when not authenticated', () => {
  const mockValue = {
    user: null,
    isAuthenticated: false,
    login: jest.fn()
  };
  
  render(
    <MockAuthProvider value={mockValue}>
      <UserMenu />
    </MockAuthProvider>
  );
  
  expect(screen.getByText('Login')).toBeInTheDocument();
});

Example: TypeScript context with full type safety

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

// Define types
interface User {
  id: number;
  name: string;
  email: string;
}

interface AuthContextType {
  user: User | null;
  isAuthenticated: boolean;
  login: (email: string, password: string) => Promise<void>;
  logout: () => void;
}

// Create typed context (null as default indicates provider required)
const AuthContext = createContext<AuthContextType | null>(null);

// Provider
export function AuthProvider({ children }: { children: ReactNode }) {
  const [user, setUser] = useState<User | null>(null);
  
  const login = async (email: string, password: string) => {
    const userData = await loginAPI(email, password);
    setUser(userData);
  };
  
  const logout = () => setUser(null);
  
  const value: AuthContextType = {
    user,
    isAuthenticated: user !== null,
    login,
    logout
  };
  
  return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
}

// Typed hook with runtime validation
export function useAuth(): AuthContextType {
  const context = useContext(AuthContext);
  
  if (context === null) {
    throw new Error('useAuth must be used within AuthProvider');
  }
  
  return context; // TypeScript knows this is AuthContextType
}

// Usage with full type safety
function Profile() {
  const { user, logout } = useAuth(); // Fully typed, autocomplete works
  
  if (!user) return null;
  
  return (
    <div>
      <h1>{user.name}</h1> {/* TypeScript knows user.name exists */}
      <p>{user.email}</p>
      <button onClick={logout}>Logout</button>
    </div>
  );
}

Context API Best Practices Summary

  • Create custom hooks with validation for consuming context
  • Always memoize context values with useMemo to prevent unnecessary re-renders
  • Split contexts by concern (state/dispatch, read/write) for performance
  • Use TypeScript for type-safe context and better developer experience
  • Prefer props for 2-3 levels, Context for 4+ levels or truly global state
  • Create custom render wrappers for testing components with context
  • Compose providers for cleaner app structure with multiple contexts