Skip to main content

Context API

Context provides a way to share data across the component tree without manually passing props at every level. It’s React’s built-in solution for “global” state.

The Problem: Prop Drilling

Without Context, passing data to deeply nested components requires threading props through every intermediate component:
┌─────────────────────────────────────────────────────────────┐
│                          App                                │
│                      theme="dark"                           │
│                           │                                 │
│                           ▼                                 │
│                        Layout                               │
│                      theme={theme}   ← Doesn't use it      │
│                           │                                 │
│                           ▼                                 │
│                       Sidebar                               │
│                      theme={theme}   ← Doesn't use it      │
│                           │                                 │
│                           ▼                                 │
│                        Menu                                 │
│                      theme={theme}   ← Doesn't use it      │
│                           │                                 │
│                           ▼                                 │
│                      MenuItem                               │
│                      theme={theme}   ← Finally uses it!    │
└─────────────────────────────────────────────────────────────┘
With Context, components can access shared data directly:
┌─────────────────────────────────────────────────────────────┐
│                   ThemeContext.Provider                     │
│                      value="dark"                           │
│                           │                                 │
│                 ┌─────────┴─────────┐                      │
│                 ▼                   ▼                       │
│              Layout              Sidebar                    │
│                 │                   │                       │
│                 ▼                   ▼                       │
│              Header              MenuItem                   │
│           useContext()        useContext()                  │
│               ↑                    ↑                        │
│               └────────────────────┘                        │
│               Direct access to "dark"                       │
└─────────────────────────────────────────────────────────────┘

Creating and Using Context

Step 1: Create the Context

// ThemeContext.js
import { createContext } from 'react';

// Default value (used when no Provider is found)
export const ThemeContext = createContext('light');

Step 2: Provide the Context

// App.jsx
import { ThemeContext } from './ThemeContext';

function App() {
  const [theme, setTheme] = useState('dark');

  return (
    <ThemeContext.Provider value={theme}>
      <Layout />
    </ThemeContext.Provider>
  );
}

Step 3: Consume the Context

// MenuItem.jsx
import { useContext } from 'react';
import { ThemeContext } from './ThemeContext';

function MenuItem({ label }) {
  const theme = useContext(ThemeContext);
  
  return (
    <li className={`menu-item ${theme}`}>
      {label}
    </li>
  );
}

Complete Example: Theme Context

Here’s a full implementation with toggling:
// contexts/ThemeContext.jsx
import { createContext, useContext, useState } from 'react';

const ThemeContext = createContext(null);

export 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}>
      {children}
    </ThemeContext.Provider>
  );
}

export function useTheme() {
  const context = useContext(ThemeContext);
  if (!context) {
    throw new Error('useTheme must be used within a ThemeProvider');
  }
  return context;
}
// App.jsx
import { ThemeProvider } from './contexts/ThemeContext';

function App() {
  return (
    <ThemeProvider>
      <div className="app">
        <Header />
        <Main />
      </div>
    </ThemeProvider>
  );
}
// components/ThemeToggle.jsx
import { useTheme } from '../contexts/ThemeContext';

function ThemeToggle() {
  const { theme, toggleTheme, isDark } = useTheme();

  return (
    <button onClick={toggleTheme}>
      {isDark ? '☀️ Light Mode' : '🌙 Dark Mode'}
    </button>
  );
}
Best Practice: Always create a custom hook (useTheme) instead of exporting the context directly. This:
  • Provides better error messages
  • Makes refactoring easier
  • Hides implementation details

Real-World Pattern: Auth Context

// contexts/AuthContext.jsx
import { createContext, useContext, useState, useEffect } from 'react';

const AuthContext = createContext(null);

export function AuthProvider({ children }) {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    // Check for existing session
    const checkAuth = async () => {
      try {
        const token = localStorage.getItem('token');
        if (token) {
          const response = await fetch('/api/auth/me', {
            headers: { Authorization: `Bearer ${token}` }
          });
          if (response.ok) {
            const userData = await response.json();
            setUser(userData);
          }
        }
      } catch (error) {
        console.error('Auth check failed:', error);
      } finally {
        setLoading(false);
      }
    };
    
    checkAuth();
  }, []);

  const login = async (email, password) => {
    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();
    localStorage.setItem('token', token);
    setUser(user);
    return user;
  };

  const logout = () => {
    localStorage.removeItem('token');
    setUser(null);
  };

  const value = {
    user,
    loading,
    isAuthenticated: !!user,
    login,
    logout
  };

  return (
    <AuthContext.Provider value={value}>
      {!loading && children}
    </AuthContext.Provider>
  );
}

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

Using Auth Context

// components/Navbar.jsx
function Navbar() {
  const { user, isAuthenticated, logout } = useAuth();

  return (
    <nav>
      <Logo />
      {isAuthenticated ? (
        <>
          <span>Welcome, {user.name}</span>
          <button onClick={logout}>Logout</button>
        </>
      ) : (
        <Link to="/login">Login</Link>
      )}
    </nav>
  );
}
// pages/LoginPage.jsx
function LoginPage() {
  const { login } = useAuth();
  const navigate = useNavigate();
  const [error, setError] = useState('');

  const handleSubmit = async (e) => {
    e.preventDefault();
    try {
      await login(email, password);
      navigate('/dashboard');
    } catch (err) {
      setError('Invalid credentials');
    }
  };

  return (/* form */);
}

Multiple Contexts

You can compose multiple contexts:
// App.jsx
function App() {
  return (
    <AuthProvider>
      <ThemeProvider>
        <NotificationProvider>
          <Router>
            <Layout />
          </Router>
        </NotificationProvider>
      </ThemeProvider>
    </AuthProvider>
  );
}

Alternative: Compose Providers

// contexts/AppProviders.jsx
function AppProviders({ children }) {
  return (
    <AuthProvider>
      <ThemeProvider>
        <NotificationProvider>
          {children}
        </NotificationProvider>
      </ThemeProvider>
    </AuthProvider>
  );
}

// App.jsx
function App() {
  return (
    <AppProviders>
      <Router>
        <Layout />
      </Router>
    </AppProviders>
  );
}

Performance Optimization

The Re-render Problem

When context value changes, ALL consuming components re-render, even if they only use part of the context:
// ❌ Problem: Updating `cart` re-renders ThemeToggle
const AppContext = createContext();

function AppProvider({ children }) {
  const [theme, setTheme] = useState('light');
  const [cart, setCart] = useState([]);
  
  return (
    <AppContext.Provider value={{ theme, setTheme, cart, setCart }}>
      {children}
    </AppContext.Provider>
  );
}

function ThemeToggle() {
  const { theme, setTheme } = useContext(AppContext);
  // Re-renders when cart changes! 😱
}

Solution 1: Split Contexts

// ✅ Separate concerns into different contexts
const ThemeContext = createContext();
const CartContext = createContext();

function App() {
  return (
    <ThemeProvider>
      <CartProvider>
        <Layout />
      </CartProvider>
    </ThemeProvider>
  );
}

function ThemeToggle() {
  const { theme, setTheme } = useTheme();
  // Only re-renders when theme changes ✅
}

Solution 2: Memoize Context Value

function ThemeProvider({ children }) {
  const [theme, setTheme] = useState('light');

  // Memoize the value object
  const value = useMemo(() => ({
    theme,
    toggleTheme: () => setTheme(t => t === 'light' ? 'dark' : 'light')
  }), [theme]);

  return (
    <ThemeContext.Provider value={value}>
      {children}
    </ThemeContext.Provider>
  );
}

Solution 3: State and Dispatch Separation

const StateContext = createContext();
const DispatchContext = createContext();

function AppProvider({ children }) {
  const [state, dispatch] = useReducer(reducer, initialState);

  return (
    <StateContext.Provider value={state}>
      <DispatchContext.Provider value={dispatch}>
        {children}
      </DispatchContext.Provider>
    </StateContext.Provider>
  );
}

// Components that only dispatch don't re-render on state changes
function AddButton() {
  const dispatch = useContext(DispatchContext);
  return <button onClick={() => dispatch({ type: 'ADD' })}>Add</button>;
}

Context with useReducer

For complex state, combine Context with useReducer:
// contexts/CartContext.jsx
const CartContext = createContext();

const cartReducer = (state, action) => {
  switch (action.type) {
    case 'ADD_ITEM':
      const existing = state.items.find(i => i.id === action.payload.id);
      if (existing) {
        return {
          ...state,
          items: state.items.map(i =>
            i.id === action.payload.id
              ? { ...i, quantity: i.quantity + 1 }
              : i
          )
        };
      }
      return {
        ...state,
        items: [...state.items, { ...action.payload, quantity: 1 }]
      };
      
    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_CART':
      return { ...state, items: [] };
      
    default:
      return state;
  }
};

export function CartProvider({ children }) {
  const [state, dispatch] = useReducer(cartReducer, { items: [] });

  const addItem = (product) => dispatch({ type: 'ADD_ITEM', payload: product });
  const removeItem = (id) => dispatch({ type: 'REMOVE_ITEM', payload: id });
  const updateQuantity = (id, quantity) => dispatch({ type: 'UPDATE_QUANTITY', payload: { id, quantity } });
  const clearCart = () => dispatch({ type: 'CLEAR_CART' });

  const total = state.items.reduce(
    (sum, item) => sum + item.price * item.quantity,
    0
  );

  const itemCount = state.items.reduce(
    (sum, item) => sum + item.quantity,
    0
  );

  return (
    <CartContext.Provider value={{
      items: state.items,
      total,
      itemCount,
      addItem,
      removeItem,
      updateQuantity,
      clearCart
    }}>
      {children}
    </CartContext.Provider>
  );
}

export const useCart = () => {
  const context = useContext(CartContext);
  if (!context) {
    throw new Error('useCart must be used within a CartProvider');
  }
  return context;
};

When to Use Context vs. Other Solutions

ScenarioRecommendation
Theme, locale, user data✅ Context
Data needed by 2-3 componentsProps (avoid over-engineering)
Complex state with many actionsContext + useReducer or Redux
Server state (API data)React Query / SWR
Frequent updates (animations)Zustand, Jotai, or refs
Very large appsRedux Toolkit, Zustand

🎯 Practice Exercises

// contexts/NotificationContext.jsx
const NotificationContext = createContext();

export function NotificationProvider({ children }) {
  const [notifications, setNotifications] = useState([]);

  const addNotification = (message, type = 'info') => {
    const id = Date.now();
    setNotifications(prev => [...prev, { id, message, type }]);
    
    // Auto-remove after 5 seconds
    setTimeout(() => {
      removeNotification(id);
    }, 5000);
  };

  const removeNotification = (id) => {
    setNotifications(prev => prev.filter(n => n.id !== id));
  };

  return (
    <NotificationContext.Provider value={{ notifications, addNotification, removeNotification }}>
      {children}
      <NotificationContainer />
    </NotificationContext.Provider>
  );
}

function NotificationContainer() {
  const { notifications, removeNotification } = useNotifications();

  return (
    <div className="notification-container">
      {notifications.map(n => (
        <div key={n.id} className={`notification notification-${n.type}`}>
          {n.message}
          <button onClick={() => removeNotification(n.id)}>×</button>
        </div>
      ))}
    </div>
  );
}

export const useNotifications = () => useContext(NotificationContext);

// Usage
function SaveButton() {
  const { addNotification } = useNotifications();
  
  const handleSave = async () => {
    try {
      await saveData();
      addNotification('Saved successfully!', 'success');
    } catch {
      addNotification('Failed to save', 'error');
    }
  };
  
  return <button onClick={handleSave}>Save</button>;
}
// contexts/LanguageContext.jsx
const translations = {
  en: {
    greeting: 'Hello',
    goodbye: 'Goodbye',
    welcome: 'Welcome to our app'
  },
  es: {
    greeting: 'Hola',
    goodbye: 'Adiós',
    welcome: 'Bienvenido a nuestra aplicación'
  },
  fr: {
    greeting: 'Bonjour',
    goodbye: 'Au revoir',
    welcome: 'Bienvenue dans notre application'
  }
};

const LanguageContext = createContext();

export function LanguageProvider({ children }) {
  const [language, setLanguage] = useState('en');

  const t = (key) => translations[language][key] || key;

  return (
    <LanguageContext.Provider value={{ language, setLanguage, t }}>
      {children}
    </LanguageContext.Provider>
  );
}

export const useLanguage = () => useContext(LanguageContext);

// Usage
function WelcomeBanner() {
  const { t } = useLanguage();
  return <h1>{t('welcome')}</h1>;
}

function LanguageSelector() {
  const { language, setLanguage } = useLanguage();
  
  return (
    <select value={language} onChange={e => setLanguage(e.target.value)}>
      <option value="en">English</option>
      <option value="es">Español</option>
      <option value="fr">Français</option>
    </select>
  );
}

Summary

ConceptDescription
ContextShare data without passing props manually
createContextCreates a context object
ProviderComponent that provides the value
useContextHook to consume context
Custom HookBest practice for consuming context
Prop DrillingProblem Context solves
PerformanceSplit contexts to prevent unnecessary re-renders
useReducer + ContextPattern for complex state management

Next Steps

In the next chapter, you’ll learn about React Router — building multi-page applications with client-side routing!