Chapter 4: useContext

The Problem: Prop Drilling

When data needs to pass through multiple levels of components:

function App() {
    const [user, setUser] = useState({ name: 'Jean' });

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

function Layout({ user }) {
    return (
        <div>
            <Header user={user} />
            <Main user={user} />
        </div>
    );
}

function Header({ user }) {
    return <Navbar user={user} />;
}

function Navbar({ user }) {
    return <UserMenu user={user} />;  // Finally used here!
}

function UserMenu({ user }) {
    return <span>{user.name}</span>;
}

Layout and Header only forward user without using it. This is prop drilling.

The Solution: Context

Context allows you to "teleport" data through the component tree.

Step 1: Create the Context

import { createContext } from 'react';

const UserContext = createContext(null);

Step 2: Provide the Value (Provider)

function App() {
    const [user, setUser] = useState({ name: 'Jean' });

    return (
        <UserContext.Provider value={user}>
            <Layout />
        </UserContext.Provider>
    );
}

Step 3: Consume the Value (useContext)

import { useContext } from 'react';

function UserMenu() {
    const user = useContext(UserContext);

    return <span>{user.name}</span>;
}

Intermediate components no longer need to pass user:

function Layout() {
    return (
        <div>
            <Header />
            <Main />
        </div>
    );
}

function Header() {
    return <Navbar />;
}

function Navbar() {
    return <UserMenu />;
}

Complete Example: Theme

// ThemeContext.js
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');
    };

    return (
        <ThemeContext.Provider value={{ theme, toggleTheme }}>
            {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
import { ThemeProvider } from './ThemeContext';

function App() {
    return (
        <ThemeProvider>
            <Layout />
        </ThemeProvider>
    );
}
// ThemeToggle.js
import { useTheme } from './ThemeContext';

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

    return (
        <button onClick={toggleTheme}>
            Current mode: {theme}
        </button>
    );
}
// Card.js
import { useTheme } from './ThemeContext';

function Card({ children }) {
    const { theme } = useTheme();

    return (
        <div className={`card card-${theme}`}>
            {children}
        </div>
    );
}

Passing Functions via Context

You can pass functions to allow children to modify the state:

const AuthContext = createContext(null);

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

    const login = async (credentials) => {
        const response = await api.login(credentials);
        setUser(response.user);
    };

    const logout = () => {
        setUser(null);
    };

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

function useAuth() {
    return useContext(AuthContext);
}

// Usage
function LoginButton() {
    const { user, login, logout } = useAuth();

    if (user) {
        return <button onClick={logout}>Logout</button>;
    }

    return <button onClick={() => login({ email, password })}>Login</button>;
}

When to Use Context?

Good use cases:

  • Theme (dark/light mode)
  • Logged in user
  • Language / internationalization
  • Global configuration

Bad use cases:

  • Local form state
  • Data that concerns only a few nearby components
  • State that changes very frequently (performance)

Context vs Props

Criteria Props Context
Data concerns few components
Data passes through many levels
Explicit data flow
Global configuration

Multiple Contexts

You can nest multiple Providers:

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

Exercises

Exercise 1: Language Context

Create a context to manage language (fr/en) with a button to change it.

Solution:

// LanguageContext.js
import { createContext, useContext, useState } from 'react';

const LanguageContext = createContext(null);

const translations = {
    fr: { greeting: 'Bonjour', button: 'English' },
    en: { greeting: 'Hello', button: 'French' }
};

export function LanguageProvider({ children }) {
    const [lang, setLang] = useState('fr');

    const toggleLang = () => {
        setLang(prev => prev === 'fr' ? 'en' : 'fr');
    };

    const t = (key) => translations[lang][key];

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

export function useLanguage() {
    return useContext(LanguageContext);
}
// Greeting.js
function Greeting() {
    const { t, toggleLang } = useLanguage();

    return (
        <div>
            <h1>{t('greeting')}</h1>
            <button onClick={toggleLang}>{t('button')}</button>
        </div>
    );
}

Key Points to Remember

  1. Context avoids prop drilling
  2. createContext -> Provider -> useContext
  3. Create a custom hook (useTheme, useAuth) to simplify usage
  4. Use for truly global data
  5. Don't overuse: props are often clearer

Module 3 Summary

Hook Usage Triggers Re-render
useState Local state Yes
useEffect Side effects (API, events) No (but can modify state)
useRef DOM and persistent values No
useContext Global data Yes (if value changes)

<- Previous Chapter | Back to module | Next Module: Advanced React ->