Skip to main content

Documentation Index

Fetch the complete documentation index at: https://resources.devweekends.com/llms.txt

Use this file to discover all available pages before exploring further.

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. Real-world analogy: Imagine a building where the Wi-Fi password is posted in the lobby (the Provider). Any room (component) in the building can check the lobby sign directly — nobody needs to relay the password door-to-door from room to room. That relay process is prop drilling, and Context is the lobby sign that makes it unnecessary.

The Problem: Prop Drilling

Without Context, passing data to deeply nested components requires threading props through every intermediate component — even the ones that do not use the data themselves:
┌─────────────────────────────────────────────────────────────┐
│                          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

This is the biggest gotcha with Context and the reason senior developers sometimes avoid it for frequently-changing state. When context value changes, ALL consuming components re-render, even if they only use part of the context. There is no built-in way to subscribe to just a slice of the context value.
// ❌ 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;
};

Context Pitfalls

Pitfall 1 — Creating a new value object on every render: If your Provider creates the value object inline, every render creates a new reference, and all consumers re-render — even if the actual data has not changed.
// BAD: new object on every render
<ThemeContext.Provider value={{ theme, toggleTheme }}>

// GOOD: memoize the value
const value = useMemo(() => ({ theme, toggleTheme }), [theme]);
<ThemeContext.Provider value={value}>
Pitfall 2 — Using Context for frequently-changing state: Context is designed for state that changes infrequently (theme, locale, auth). If you put rapidly-changing state (mouse position, animation frames, typing input) into Context, every consumer re-renders on every change. For high-frequency updates, use useRef with a subscription pattern, or a library like Zustand or Jotai that supports fine-grained subscriptions.Pitfall 3 — Forgetting the Provider: If a component calls useContext(MyContext) but is not wrapped in a MyContext.Provider, it silently receives the default value from createContext(). If the default is undefined, you get cryptic “cannot read property of undefined” errors. Always use a custom hook with an error boundary: if (!context) throw new Error('useMyContext must be used within a MyProvider').Pitfall 4 — Nesting providers in the wrong order: If ContextA’s Provider needs data from ContextB, ContextB’s Provider must be the outer wrapper. Getting the order wrong means ContextA’s Provider tries to call useContext(ContextB) outside of ContextB’s Provider tree and gets the default value.

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!

Interview Deep-Dive

Strong Answer: This is the fundamental performance limitation of Context, and there is no built-in way to subscribe to a “slice” of context. When the Provider’s value changes (by reference), every component calling useContext for that context re-renders — even if it only reads theme and cart was the thing that changed.Three solutions, each with different tradeoffs:First, split contexts. Instead of one AppContext with theme and cart, create ThemeContext and CartContext. Components consuming only theme are unaffected by cart changes. This is the simplest and most recommended approach. The cost is more Provider components wrapping your app, but that has negligible runtime impact.Second, memoize the context value. Wrap the value object in useMemo: const value = useMemo(() => ({ theme, toggleTheme }), [theme]). This prevents re-renders when the provider component re-renders for unrelated reasons (like a parent state change), because the value reference stays stable. But it does NOT prevent re-renders when the actual context data changes — that is by design.Third, separate state and dispatch contexts. Inspired by Redux, you create two contexts: one for the state (which changes), one for the dispatch function (which is stable). Components that only dispatch actions (like an “Add to Cart” button) consume the dispatch context and never re-render when state changes. Components that read state consume the state context. This dramatically reduces re-renders in action-heavy UIs.What most people miss: you can also use React.memo on child components of the consumer to prevent their subtree from re-rendering. If CartBadge uses context but wraps its children in memo, the children are protected even when the badge re-renders.Follow-up: Some developers avoid Context for frequently changing values (like a real-time cursor position) and use Zustand or Jotai instead. Why?Context re-renders ALL consumers on every value change, and there is no granular subscription mechanism. For a cursor position updating at 60fps, every component in the context tree re-renders 60 times per second. Even with React.memo on most children, the consumers themselves still re-render and run their function bodies.Zustand and Jotai use external stores with selector-based subscriptions. A component subscribes to a specific slice of state: useStore(state => state.cursor). When cursor changes, only components that selected cursor re-render. Components that selected state.theme are completely untouched. This is possible because these libraries bypass React’s context mechanism and use useSyncExternalStore (React 18) to trigger granular re-renders.The practical threshold: if context value changes more than a few times per second or has many consumers (50+), switch to an external store. For infrequently changing values like theme, locale, and auth status, Context is perfectly adequate and simpler.
Strong Answer: Provider hell is when your root component has 10+ nested Providers, each wrapping the previous one. It looks like a pyramid of doom in JSX and makes the component tree hard to reason about in DevTools. The order of providers can matter (an AuthProvider that depends on an ApiProvider must be nested inside it), which creates implicit dependency chains.The simplest fix is a ComposeProviders utility component that takes an array of providers and nests them automatically:
function ComposeProviders({ providers, children }) {
  return providers.reduceRight(
    (acc, Provider) => <Provider>{acc}</Provider>,
    children
  );
}

// Usage
<ComposeProviders providers={[ThemeProvider, AuthProvider, CartProvider, NotificationProvider]}>
  <App />
</ComposeProviders>
This is cleaner but does not solve the underlying question: should you have this many contexts?For large applications, I evaluate each context: does it need to be global? Auth and theme are truly global. A shopping cart might be scoped to the e-commerce section. Form state should be local. Moving contexts to the lowest common ancestor reduces the blast radius of re-renders and makes the app structure clearer.Another approach: consolidate related contexts using useReducer. Instead of separate UserContext, PreferencesContext, and NotificationsContext, combine them into a single AppContext with a reducer that handles all three concerns. Split the state and dispatch into separate contexts for performance. This reduces Provider count while maintaining granular subscriptions.Follow-up: How do you test components that depend on multiple contexts?Create a test utility function that wraps components with the necessary providers pre-configured with test-friendly defaults:
function renderWithProviders(ui, { user = mockUser, theme = 'light' } = {}) {
  return render(
    <AuthProvider initialUser={user}>
      <ThemeProvider initialTheme={theme}>
        {ui}
      </ThemeProvider>
    </AuthProvider>
  );
}
This pattern (which React Testing Library’s docs recommend) lets each test specify only the overrides it cares about. For unit tests of a component that uses useAuth, you can mock the hook directly: vi.mock('./useAuth', () => ({ useAuth: () => ({ user: testUser }) })). This avoids setting up the full provider tree when you only need one context value.For integration tests, use the real providers with controlled initial state. This tests that the context wiring actually works end-to-end, catching bugs like a missing Provider or wrong context reference.
Strong Answer: Context plus useReducer is essentially a lightweight, built-in Redux. You get a central store (context value), actions (dispatched objects), a reducer (pure function), and subscriptions (useContext consumers). For small to medium apps, this is often all you need.Context stops being sufficient at three thresholds. First, performance: when you have many consumers and frequent state updates, Context’s all-or-nothing re-render behavior becomes a bottleneck. Redux’s useSelector with shallow equality checking only re-renders components whose selected slice changed. For a dashboard with 50 widgets reading different parts of global state, Redux prevents 49 unnecessary re-renders that Context would trigger.Second, middleware and side effects: Redux Toolkit’s createAsyncThunk and middleware pipeline give you a structured way to handle async logic, logging, error reporting, and optimistic updates. With Context, you manually implement all of this in your Provider or custom hooks. It is doable but messy at scale.Third, developer tools: Redux DevTools offer time-travel debugging, action replay, state diffs, and export/import of state snapshots. This is invaluable for debugging complex state transitions in production. Context has no equivalent tooling.My decision framework: start with local state. When multiple components need shared state, use Context. When you hit performance issues with Context or need more than 5-6 actions with complex async logic, evaluate Redux Toolkit or Zustand. For server state specifically (API data with caching, pagination, optimistic updates), skip Redux entirely and use React Query.Follow-up: Zustand has gained popularity over Redux for many teams. What makes it different and when would you still choose Redux?Zustand is dramatically simpler: no Providers, no wrapping your app, no actions/reducers pattern. You create a store with create(), define state and updaters as a plain object, and consume with a hook that supports selectors. The entire API is 5 functions. Bundle size is around 1KB.I would still choose Redux Toolkit when: the team is large and needs enforced patterns (actions, reducers, slices give structure that Zustand does not impose), when you need the DevTools time-travel debugging for complex state machines, when you have significant async logic that benefits from createAsyncThunk and middleware, or when the project already uses Redux and migration cost outweighs Zustand’s simplicity.I would choose Zustand when: the team is small, state logic is straightforward, bundle size matters (Zustand is 10x smaller than Redux Toolkit), and you value API simplicity over structural patterns. Zustand is particularly good for state that is not connected to React at all (like a game engine or a shared state between React and vanilla JS).