Skip to content

Context API — React's Built-in Prop-Drilling Solution

DodaTech Updated 2026-06-28 6 min read

In this tutorial, you will learn about Context API. We cover key concepts, practical examples, and best practices to help you master this topic.

React Context API shares state across components without prop drilling, providing a built-in alternative to external state management libraries for simpler use cases like themes, auth, and locale.

What You'll Learn

By the end of this tutorial, you will understand how to create and use Context, when to use it versus Redux, performance implications, and patterns for organizing Context providers.

Why It Matters

Prop drilling — passing props through intermediate components — is one of the most common sources of React code clutter. Context eliminates this for app-wide state, keeping your component tree clean without adding external dependencies.

Real-World Use

A multi-tenant SPA uses Context for the current tenant configuration (logo, colors, branding). When a user switches tenants, a single context update propagates to the entire app, updating the header, sidebar, and content area without prop drilling through 5 levels of components.

Context Architecture

Context Architecture
    App (Provider wraps all children)
    └── ThemeProvider (provides theme)
        └── AuthProvider (provides user)
            └── Layout
                ├── Header (usesAuth)
                ├── Sidebar (usesAuth, usesTheme)
                └── Main (usesTheme)
                    ├── Profile (usesAuth)
                    └── Settings (usesTheme)

Each component calls useContext to access
values from the nearest Provider above it

Think of Context like an electrical grid. Providers are power stations that supply electricity (state). Any building (component) connected to the grid can draw power without running a cable from the power station directly.

Creating and Using Context

// contexts/ThemeContext.js
import { createContext, useContext, useState, useEffect } from 'react';

const ThemeContext = createContext(null);

export function ThemeProvider({ children }) {
    const [theme, setTheme] = useState(() => {
        return localStorage.getItem('theme') || 'light';
    });

    useEffect(() => {
        localStorage.setItem('theme', theme);
        document.documentElement.setAttribute('data-theme', theme);
    }, [theme]);

    const toggleTheme = () => {
        setTheme(prev => prev === 'light' ? 'dark' : 'light');
    };

    return (
        <ThemeContext.Provider value={{ theme, toggleTheme, setTheme }}>
            {children}
        </ThemeContext.Provider>
    );
}

export function useTheme() {
    const context = useContext(ThemeContext);
    if (!context) {
        throw new Error('useTheme must be used within ThemeProvider');
    }
    return context;
}

// App.js
function App() {
    return (
        <ThemeProvider>
            <Header />
            <Main />
        </ThemeProvider>
    );
}

// Header.js
function Header() {
    const { theme, toggleTheme } = useTheme();
    return (
        <header className={`header-${theme}`}>
            <button onClick={toggleTheme}>
                Switch to {theme === 'light' ? 'dark' : 'light'} mode
            </button>
        </header>
    );
}

Multiple Context Providers

// contexts/AuthContext.js
import { createContext, useContext, useState, useCallback } from 'react';

const AuthContext = createContext(null);

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

    const login = useCallback(async (email, password) => {
        setLoading(true);
        try {
            const response = await fetch('/api/login', {
                method: 'POST',
                body: JSON.stringify({ email, password }),
                headers: { 'Content-Type': 'application/json' }
            });
            const data = await response.json();
            setUser(data.user);
            return data;
        } finally {
            setLoading(false);
        }
    }, []);

    const logout = useCallback(() => {
        setUser(null);
        fetch('/api/logout', { method: 'POST' });
    }, []);

    return (
        <AuthContext.Provider value={{ user, loading, login, logout }}>
            {children}
        </AuthContext.Provider>
    );
}

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

// contexts/CartContext.js
const CartContext = createContext(null);

export function CartProvider({ children }) {
    const [items, setItems] = useState([]);

    const addItem = (product) => {
        setItems(prev => {
            const existing = prev.find(i => i.id === product.id);
            if (existing) {
                return prev.map(i =>
                    i.id === product.id
                        ? { ...i, quantity: i.quantity + 1 }
                        : i
                );
            }
            return [...prev, { ...product, quantity: 1 }];
        });
    };

    const totalItems = items.reduce((sum, i) => sum + i.quantity, 0);

    return (
        <CartContext.Provider value={{ items, addItem, totalItems }}>
            {children}
        </CartContext.Provider>
    );
}

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

// App.js with multiple providers
function App() {
    return (
        <ThemeProvider>
            <AuthProvider>
                <CartProvider>
                    <Header />
                    <Main />
                </CartProvider>
            </AuthProvider>
        </ThemeProvider>
    );
}

Context Best Practices

// Avoid passing objects that recreate every render
// BAD: creates new object each render
function BadProvider({ children }) {
    const [user, setUser] = useState(null);
    return (
        <AuthContext.Provider value={{ user, setUser }}>
            {children}
        </AuthContext.Provider>
    );
}

// GOOD: memoize or use useMemo
function GoodProvider({ children }) {
    const [user, setUser] = useState(null);
    const value = useMemo(() => ({ user, setUser }), [user]);

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

// Split contexts to avoid unnecessary re-renders
// If theme changes, auth consumers should not re-render
function App() {
    return (
        <ThemeProvider>  {/* Theme changes only re-render theme consumers */}
            <AuthProvider>  {/* Auth changes only re-render auth consumers */}
                <CartProvider>  {/* Cart changes only re-render cart consumers */}
                    <Main />
                </CartProvider>
            </AuthProvider>
        </ThemeProvider>
    );
}

Context vs Redux Decision

function chooseStateManagement({ requirements }) {
    const {
        frequentUpdates,   // State changes many times per second
        complexLogic,      // Async workflows, middleware
        manyConsumers,     // 10+ components read the same state
        teamSize,          // Number of developers
        trackingNeeded     // Need action logging and replay
    } = requirements;

    if (frequentUpdates && complexLogic && manyConsumers && trackingNeeded) {
        return 'Redux (or Zustand)';
    }

    if (!frequentUpdates && !complexLogic && manyConsumers) {
        return 'Context API';
    }

    if (teamSize <= 3 && !trackingNeeded) {
        return 'Context API';
    }

    return 'Component state or lifted state';
}

Common Mistakes

  1. Creating a new context value on every render. Passing an inline object value={{ user, login }} creates a new reference each render, causing all consumers to re-render. Memoize the value.
  2. Putting everything in one context. If theme and auth data are in the same context, changing the theme re-renders all auth consumers. Split contexts by domain.
  3. Using context for frequently updating data. If state changes many times per second (animation, mouse position), Context causes performance issues. Use refs or a store with selectors.
  4. Not providing a default value that makes sense. The default value passed to createContext is used when a component is not wrapped in a Provider. Provide a meaningful default or throw.
  5. Nesting providers too deeply. More than 5-6 nested Provider components becomes hard to manage. Consider composition or colocation.

Practice Questions

  1. What problem does Context API solve?
  2. Why should you avoid putting everything in a single context?
  3. How do you prevent unnecessary re-renders with Context?
  4. When should you choose Context over Redux?
  5. What happens when a component calls useContext without a matching Provider?

Challenge: Build an application with three separate contexts: ThemeContext (light/dark with persistence), AuthContext (login/logout with mock API), and NotificationContext (toast notifications). Each context should be in its own file with a custom hook. Demonstrate that changing one context does not re-render consumers of the others.

FAQ

Does Context replace Redux?

No. Context is a dependency injection mechanism, not a state management solution. It does not handle complex logic, middleware, or performance optimization out of the box.

Can I have nested Providers with the same context?

Yes. A component will receive the value from the nearest Provider ancestor. This is useful for overriding context values in specific subtrees.

Does Context cause performance issues?

Yes, if not used carefully. Every context value change re-renders all consumers. Memoize values, split contexts, and use React.memo on consumer components.

Can Context be used with class components?

Yes. Class components use ThemeContext.Consumer render prop pattern or the static contextType property. Hooks are the modern approach.

How do I test components that use Context?

Wrap the component in the appropriate Provider during testing. Pass controlled values to the Provider to test different states.

Mini Project

Build a settings panel SPA using Context API: ThemeContext (light/dark, persisted to localStorage), FontSizeContext (small/medium/large), and LanguageContext (en/es/fr). Each context should have its own Provider and custom hook. Create a settings page where changing any setting updates the UI immediately.

What's Next

You mastered state sharing. Now learn about component lifecycle — understanding when components mount, update, and unmount in your SPA.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro