Skip to main content

🌐 useContext Hook

Welcome to one of React's most powerful features for sharing data! Have you ever found yourself passing props down through multiple layers of components, just to get data to a deeply nested child? That's called "prop drilling," and it's a pain. The useContext Hook solves this problem elegantly by creating a "wormhole" for your data - allowing any component to access shared values without passing props through every level. Think of it like a radio broadcast: instead of passing a message person-to-person through a chain, you broadcast it, and anyone tuned in can receive it. Let's learn how to use this superpower wisely! πŸ“‘

🎯 Learning Objectives

By the end of this lesson, you will be able to:

  • Understand the prop drilling problem and why it matters
  • Create a Context with proper TypeScript typing
  • Provide context values to component trees
  • Consume context using the useContext Hook
  • Implement common context patterns (theme, authentication, etc.)
  • Combine useContext with useReducer for state management
  • Understand when to use context vs props
  • Avoid common context pitfalls and performance issues
  • Structure context for scalable applications

Estimated Time: 75-90 minutes

Project: Build a theme switcher and authentication context

πŸ“‘ In This Lesson

⛏️ The Prop Drilling Problem

Before we dive into Context, let's understand the problem it solves. Prop drilling occurs when you need to pass data through many layers of components to reach a deeply nested child.

A Real-World Scenario

Imagine you're building an app with user information that needs to be accessed by components at different levels:

graph TD A[App - has user data] --> B[Dashboard] B --> C[Sidebar] C --> D[UserMenu] D --> E[UserProfile - needs user data!] B --> F[MainContent] F --> G[Header - needs user data!] style A fill:#667eea,stroke:#333,stroke-width:2px,color:#fff style E fill:#48bb78,stroke:#333,stroke-width:2px,color:#fff style G fill:#48bb78,stroke:#333,stroke-width:2px,color:#fff

The Prop Drilling Approach

Without Context, you'd have to pass the user data through every component in the chain:

// ❌ Prop drilling - passing through components that don't need the data

interface User {
    id: number;
    name: string;
    email: string;
}

// App component - has the user data
function App() {
    const [user, setUser] = useState<User>({
        id: 1,
        name: 'John Doe',
        email: 'john@example.com'
    });

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

// Dashboard - doesn't use user, just passes it along
function Dashboard({ user }: { user: User }) {
    return (
        <div>
            <Sidebar user={user} />
            <MainContent user={user} />
        </div>
    );
}

// Sidebar - doesn't use user, just passes it along
function Sidebar({ user }: { user: User }) {
    return (
        <div>
            <UserMenu user={user} />
        </div>
    );
}

// UserMenu - doesn't use user, just passes it along
function UserMenu({ user }: { user: User }) {
    return <UserProfile user={user} />;
}

// UserProfile - FINALLY uses the user data!
function UserProfile({ user }: { user: User }) {
    return <div>{user.name}</div>;
}

⚠️ Problems with Prop Drilling

  • Verbose - Lots of repetitive prop declarations
  • Fragile - Adding props requires updating every component in the chain
  • Confusing - Components receive props they don't use
  • Hard to Refactor - Moving components means updating all props
  • Tight Coupling - Intermediate components depend on data they don't need

The Visual Problem

Here's what prop drilling looks like visually:

⚑ Interactive: Prop Drilling vs Context

Click the buttons to see data flow through props versus context

Data Flow Comparison ❌ Prop Drilling App (has user data) Dashboard (passes through) Sidebar (passes through) UserMenu (passes through) UserProfile (uses data!) πŸ“¦ βœ… Context (Wormhole!) UserContext.Provider App (broadcasts user) Dashboard (no props!) Sidebar (no props!) UserMenu (no props!) UserProfile (reads context) πŸ“‘ Click a button to visualize data flow
sequenceDiagram participant App participant Dashboard participant Sidebar participant UserMenu participant UserProfile App->>Dashboard: user prop Dashboard->>Sidebar: user prop (not used) Sidebar->>UserMenu: user prop (not used) UserMenu->>UserProfile: user prop UserProfile->>UserProfile: Finally! Use user data Note over Dashboard,UserMenu: These components don't
need user, but must
pass it through

πŸ’‘ When Does Prop Drilling Become a Problem?

  • Data needs to pass through 3+ component levels
  • Many components need the same data
  • Intermediate components don't use the data
  • The data changes frequently
  • You find yourself adding props to components just to pass them down

Real-World Examples

Common scenarios where prop drilling becomes painful:

Scenario What Needs Sharing Who Needs It
Theme Dark/light mode preference Every styled component
Authentication Current user, login status Protected routes, user menus, etc.
Language Current locale (en, es, fr) All text-displaying components
Shopping Cart Cart items, total, quantity Product pages, cart icon, checkout
Settings User preferences Various UI components

🌐 Introduction to Context

React Context is the solution to prop drilling. It provides a way to share values between components without explicitly passing props through every level of the tree.

πŸ“– Definition

Context: A feature in React that provides a way to pass data through the component tree without having to pass props down manually at every level. It creates a "global" scope for a specific part of your component tree.

The Context Solution

With Context, components can "broadcast" data, and any component in the tree can "tune in" to receive it:

graph TD A[App with Context Provider] -.broadcasts user.-> B[Dashboard] B --> C[Sidebar] C --> D[UserMenu] D --> E[UserProfile - reads from context!] B --> F[MainContent] F --> G[Header - reads from context!] A -.-> E A -.-> G style A fill:#667eea,stroke:#333,stroke-width:2px,color:#fff style E fill:#48bb78,stroke:#333,stroke-width:2px,color:#fff style G fill:#48bb78,stroke:#333,stroke-width:2px,color:#fff

Notice how Dashboard, Sidebar, and UserMenu don't need to pass the user prop anymore! The components that need the data can directly access it from context.

How Context Works

Context has three main parts:

The Three Pieces of Context

  1. Create Context - Define what data will be shared
    const UserContext = createContext(defaultValue);
  2. Provide Context - Wrap components that need access
    <UserContext.Provider value={user}>
        {children}
    </UserContext.Provider>
  3. Consume Context - Read the value in components
    const user = useContext(UserContext);

Context Flow Visualization

sequenceDiagram participant Create as 1. Create Context participant Provider as 2. Provider participant Tree as Component Tree participant Consumer as 3. Consumer (useContext) Create->>Provider: Define context Provider->>Tree: Wrap components Tree->>Consumer: Component needs data Consumer->>Provider: Read value Provider->>Consumer: Return current value

Real-World Analogy

Think of Context like a radio broadcast system:

Radio Broadcast React Context
Radio Station Context Provider
Broadcast Signal Context Value
Radio Receiver useContext Hook
Tune to Frequency Import and use context
Broadcast Range Provider's component tree

Just like how any radio in range can tune into a station without being physically connected, any component within a Provider can access the context value without receiving props!

βœ… Benefits of Context

  • No Prop Drilling - Skip intermediate components
  • Cleaner Code - Less boilerplate, more focused components
  • Easier Refactoring - Move components without updating props
  • Shared State - Multiple components access same data
  • Loose Coupling - Components only depend on data they use
  • Better Organization - Group related data together

⚠️ When NOT to Use Context

Context isn't always the answer. Don't use it when:

  • Data only needs to go 1-2 levels down (use props)
  • You're just trying to avoid typing props (that's what props are for!)
  • The data changes very frequently and affects many components (may cause performance issues)
  • The data is only used by a single component
  • You need to optimize re-renders carefully (Context re-renders all consumers)

Context vs Props vs State Management

Solution Use When Example
Props Parent-child communication, 1-2 levels Passing onClick to a button
Context Data needed by many components, deep nesting Theme, auth, language
State Management
(Redux, Zustand)
Complex global state, time travel, middleware Large e-commerce app state

πŸ”¨ Creating Context

Let's learn how to create Context step by step. We'll use TypeScript to ensure type safety throughout.

Basic Context Creation

The first step is to create a context using createContext:

import { createContext } from 'react';

// Simple value context
const ThemeContext = createContext('light');

// Object context
const UserContext = createContext({
    name: 'Guest',
    isLoggedIn: false
});

createContext takes a default value that's used when a component tries to access context without a Provider. This default is mostly useful for testing.

Context with TypeScript

For TypeScript, we need to properly type our context:

import { createContext } from 'react';

// Define the shape of your context value
interface User {
    id: number;
    name: string;
    email: string;
}

// Create context with type and default value
const UserContext = createContext<User>({
    id: 0,
    name: 'Guest',
    email: ''
});

πŸ’‘ TypeScript Tip

The generic <User> tells TypeScript what type of value the context holds. This gives you autocomplete and type checking when using the context!

Context with Functions

Context can hold more than just data - it can include functions too:

import { createContext } from 'react';

interface ThemeContextType {
    theme: 'light' | 'dark';
    toggleTheme: () => void;
}

const ThemeContext = createContext<ThemeContextType>({
    theme: 'light',
    toggleTheme: () => {
        console.warn('toggleTheme called outside of Provider');
    }
});

Context with Undefined Default

Sometimes you don't want a meaningful default value. Use undefined and force components to check:

import { createContext } from 'react';

interface User {
    id: number;
    name: string;
}

// Allow undefined - requires Provider
const UserContext = createContext<User | undefined>(undefined);

// Later, when consuming:
const user = useContext(UserContext);
if (!user) {
    throw new Error('useUser must be used within UserProvider');
}

βœ… When to Use Undefined Default

Use undefined as default when:

  • Context must always be used within a Provider
  • You want to catch mistakes early (forgetting Provider)
  • A fake default value doesn't make sense
  • You want to enforce proper setup

File Organization

Best practice is to create contexts in separate files:

// src/contexts/UserContext.tsx
import { createContext } from 'react';

export interface User {
    id: number;
    name: string;
    email: string;
}

export interface UserContextType {
    user: User | null;
    login: (user: User) => void;
    logout: () => void;
}

export const UserContext = createContext<UserContextType | undefined>(
    undefined
);

This keeps your contexts organized and makes them easy to import:

// In any component
import { UserContext } from '@/contexts/UserContext';
import { useContext } from 'react';

function MyComponent() {
    const userContext = useContext(UserContext);
    // ...
}

πŸ“€ Providing Context Values

Creating a context is just the first step. To make the context value available to components, you need to wrap them in a Provider. Think of the Provider as the broadcasting station that transmits the signal.

Basic Provider Usage

Every context comes with a Provider component that accepts a value prop:

import { createContext } from 'react';

const ThemeContext = createContext('light');

function App() {
    return (
        <ThemeContext.Provider value="dark">
            <Header />
            <MainContent />
            <Footer />
        </ThemeContext.Provider>
    );
}

// Now Header, MainContent, Footer (and all their children)
// can access the theme value "dark"

πŸ’‘ Provider Scope

The Provider's scope is its entire component tree. Any component inside the Provider (at any nesting level) can access the context value. Components outside the Provider cannot.

Provider with State

Usually, you'll want the context value to be dynamic, not static. Use state in the Provider:

import { createContext, useState } from 'react';

interface User {
    id: number;
    name: string;
}

const UserContext = createContext<User | null>(null);

function App() {
    const [user, setUser] = useState<User | null>(null);

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

// Now the user state is accessible throughout the Dashboard tree

Provider with Multiple Values

You can provide multiple related values by wrapping them in an object:

import { createContext, useState } from 'react';

interface User {
    id: number;
    name: string;
}

interface UserContextType {
    user: User | null;
    login: (user: User) => void;
    logout: () => void;
}

const UserContext = createContext<UserContextType | undefined>(undefined);

function App() {
    const [user, setUser] = useState<User | null>(null);

    const login = (newUser: User) => {
        setUser(newUser);
    };

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

    const contextValue = {
        user,
        login,
        logout
    };

    return (
        <UserContext.Provider value={contextValue}>
            <Dashboard />
        </UserContext.Provider>
    );
}

⚠️ Provider Value Warning

Be careful with the value prop! If you pass an object literal or create a new object on every render, you'll cause unnecessary re-renders:

// ❌ Bad - creates new object every render
<UserContext.Provider value={{ user, login, logout }}>

// βœ… Good - stable reference
const contextValue = { user, login, logout };
<UserContext.Provider value={contextValue}>

We'll cover this more in the performance section!

Custom Provider Component Pattern

A common pattern is to create a custom Provider component that encapsulates the logic:

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

interface User {
    id: number;
    name: string;
}

interface UserContextType {
    user: User | null;
    login: (user: User) => void;
    logout: () => void;
}

export const UserContext = createContext<UserContextType | undefined>(undefined);

// Custom Provider component
export function UserProvider({ children }: { children: ReactNode }) {
    const [user, setUser] = useState<User | null>(null);

    const login = (newUser: User) => {
        setUser(newUser);
        localStorage.setItem('user', JSON.stringify(newUser));
    };

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

    const value = { user, login, logout };

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

// Now use the custom Provider
function App() {
    return (
        <UserProvider>
            <Dashboard />
        </UserProvider>
    );
}

βœ… Custom Provider Benefits

  • Encapsulation - Logic lives with the context
  • Reusability - Easy to use across apps
  • Cleaner App Component - Less clutter in main component
  • Single Responsibility - Provider handles one concern
  • Easier Testing - Test provider independently

Multiple Providers

You can nest multiple Providers for different contexts:

function App() {
    return (
        <ThemeProvider>
            <UserProvider>
                <LanguageProvider>
                    <Dashboard />
                </LanguageProvider>
            </UserProvider>
        </ThemeProvider>
    );
}

// Dashboard and all children can access theme, user, and language contexts

πŸ’‘ Provider Order

The order of Providers usually doesn't matter, unless one Provider depends on another's context. Put independent Providers in any order, but if ThemeProvider needs user data, put UserProvider outside.

Scoped Providers

You can have multiple Providers of the same context in different parts of your tree:

function App() {
    return (
        <div>
            {/* Light theme section */}
            <ThemeContext.Provider value="light">
                <Header />
            </ThemeContext.Provider>

            {/* Dark theme section */}
            <ThemeContext.Provider value="dark">
                <MainContent />
            </ThemeContext.Provider>

            {/* Uses default or nearest Provider */}
            <Footer />
        </div>
    );
}

// Header uses light theme, MainContent uses dark theme

Components use the value from the nearest Provider above them in the tree.

Provider Placement Strategy

graph TD A[Where to Place Provider?] --> B{Who needs the data?} B -->|Entire app| C[Wrap App root] B -->|Specific section| D[Wrap that section] B -->|Multiple sections| E[Consider multiple Providers] C --> F[Performance impact: High] D --> G[Performance impact: Low] E --> H[Performance impact: Medium] style C fill:#667eea,stroke:#333,stroke-width:2px,color:#fff style D fill:#48bb78,stroke:#333,stroke-width:2px,color:#fff style E fill:#f093fb,stroke:#333,stroke-width:2px,color:#fff

🎯 Interactive: Provider Scope Explorer

Click on different Provider scopes to see which components can access the context

Provider Scope Determines Context Access App (Root) ThemeProvider Header Navigation MainContent Sidebar Menu UserWidget ThemeToggle Article Content Comments Footer Has Context Access No Access Select a Provider scope below

πŸ“₯ Consuming Context

Now that we know how to provide context, let's learn how to consume it using the useContext Hook. This is where the magic happens!

Basic useContext Usage

The useContext Hook reads the value from the nearest Provider above it:

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

function Header() {
    const theme = useContext(ThemeContext);
    
    return (
        <header className={theme === 'dark' ? 'dark' : 'light'}>
            <h1>My App</h1>
        </header>
    );
}

πŸ’‘ How useContext Works

  1. Import the context you want to use
  2. Call useContext with that context
  3. Get back the current value from the Provider
  4. Use the value in your component

Consuming Object Context

When your context provides an object with multiple values:

import { useContext } from 'react';
import { UserContext } from './UserContext';

function UserProfile() {
    const userContext = useContext(UserContext);
    
    // Destructure for cleaner code
    const { user, login, logout } = userContext;
    
    if (!user) {
        return (
            <button onClick={() => login({ id: 1, name: 'John' })}>
                Login
            </button>
        );
    }
    
    return (
        <div>
            <p>Welcome, {user.name}!</p>
            <button onClick={logout}>Logout</button>
        </div>
    );
}

Multiple Context Consumption

A component can consume multiple contexts:

import { useContext } from 'react';
import { ThemeContext } from './ThemeContext';
import { UserContext } from './UserContext';
import { LanguageContext } from './LanguageContext';

function Dashboard() {
    const theme = useContext(ThemeContext);
    const { user } = useContext(UserContext);
    const { language } = useContext(LanguageContext);
    
    return (
        <div className={`dashboard ${theme}`}>
            <h1>{language === 'en' ? 'Dashboard' : 'Tablero'}</h1>
            <p>User: {user?.name}</p>
        </div>
    );
}

Custom Hook Pattern

A best practice is to create a custom hook for each context:

// UserContext.tsx
import { createContext, useContext, useState, ReactNode } from 'react';

interface User {
    id: number;
    name: string;
}

interface UserContextType {
    user: User | null;
    login: (user: User) => void;
    logout: () => void;
}

const UserContext = createContext<UserContextType | undefined>(undefined);

// Custom Provider
export function UserProvider({ children }: { children: ReactNode }) {
    const [user, setUser] = useState<User | null>(null);

    const login = (newUser: User) => setUser(newUser);
    const logout = () => setUser(null);

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

// Custom Hook - this is the magic!
export function useUser() {
    const context = useContext(UserContext);
    
    if (context === undefined) {
        throw new Error('useUser must be used within UserProvider');
    }
    
    return context;
}

// Now in components:
function MyComponent() {
    const { user, login, logout } = useUser(); // Clean and type-safe!
    // ...
}

βœ… Custom Hook Benefits

  • Error Checking - Catches missing Provider at runtime
  • Type Safety - No undefined checks needed
  • Cleaner Imports - Import one hook instead of context + useContext
  • Consistent API - Every context has the same usage pattern
  • Better DX - Autocomplete works perfectly

Context with Computed Values

You can derive values based on context in the consuming component:

function TodoStats() {
    const { todos } = useTodos();
    
    // Derive values from context
    const totalTodos = todos.length;
    const completedTodos = todos.filter(t => t.completed).length;
    const activeTodos = totalTodos - completedTodos;
    const completionRate = totalTodos > 0 
        ? Math.round((completedTodos / totalTodos) * 100) 
        : 0;
    
    return (
        <div>
            <p>Total: {totalTodos}</p>
            <p>Completed: {completedTodos}</p>
            <p>Active: {activeTodos}</p>
            <p>Completion: {completionRate}%</p>
        </div>
    );
}

Conditional Context Usage

Sometimes you want to use context only if it's available:

// Optional context hook
export function useOptionalUser() {
    return useContext(UserContext);
}

function UserGreeting() {
    const userContext = useOptionalUser();
    
    // Handle both cases: inside and outside Provider
    if (!userContext || !userContext.user) {
        return <p>Welcome, Guest!</p>;
    }
    
    return <p>Welcome, {userContext.user.name}!</p>;
}

⚠️ Common Mistake

Don't call useContext conditionally or in loops:

// ❌ Wrong - breaks Rules of Hooks
if (someCondition) {
    const theme = useContext(ThemeContext);
}

// βœ… Correct - call at top level, use conditionally
const theme = useContext(ThemeContext);
if (someCondition) {
    // use theme
}

Context Consumer Flow

sequenceDiagram participant Component participant useContext participant Provider Component->>useContext: Call useContext(MyContext) useContext->>Provider: Find nearest Provider Provider->>useContext: Return current value useContext->>Component: Return value Component->>Component: Use value in render Note over Provider: If Provider value changes Provider->>Component: Trigger re-render Component->>useContext: Get updated value

πŸ” Interactive: How useContext Finds Its Value

Watch how useContext traverses up the component tree to find the nearest Provider

useContext Provider Lookup App <ThemeContext.Provider> βœ” Dashboard Sidebar Header Content Menu UserProfile ThemeToggle useContext(ThemeContext) How useContext Works Click "Trace Lookup" to see how React finds the Provider

πŸ”· Typing Context with TypeScript

TypeScript makes Context even more powerful by ensuring type safety. Let's explore the best patterns for typing contexts.

Basic Context Typing

The simplest approach - define an interface and use it:

import { createContext } from 'react';

// Define the shape of your context
interface ThemeContextType {
    theme: 'light' | 'dark';
    toggleTheme: () => void;
}

// Create context with type
const ThemeContext = createContext<ThemeContextType>({
    theme: 'light',
    toggleTheme: () => {}
});

Context with Undefined (Recommended)

The safest pattern - force Provider usage with undefined default:

import { createContext, useContext } from 'react';

interface User {
    id: number;
    name: string;
    email: string;
}

interface UserContextType {
    user: User | null;
    setUser: (user: User | null) => void;
}

// Create context that allows undefined
const UserContext = createContext<UserContextType | undefined>(undefined);

// Custom hook with type guard
export function useUser(): UserContextType {
    const context = useContext(UserContext);
    
    if (context === undefined) {
        throw new Error('useUser must be used within UserProvider');
    }
    
    // Now TypeScript knows context is UserContextType, not undefined!
    return context;
}

βœ… Why This Pattern is Best

  • Runtime Safety - Catches missing Provider immediately
  • No Fake Defaults - Don't need meaningless default values
  • Type Safety - Custom hook returns non-undefined type
  • Clear Errors - Helpful error message for developers

Complete Typed Context Example

Here's a full example with all TypeScript best practices:

// UserContext.tsx
import { 
    createContext, 
    useContext, 
    useState, 
    ReactNode,
    useCallback 
} from 'react';

// 1. Define data types
export interface User {
    id: number;
    name: string;
    email: string;
    role: 'admin' | 'user';
}

// 2. Define context type
interface UserContextType {
    user: User | null;
    isLoggedIn: boolean;
    login: (user: User) => void;
    logout: () => void;
    updateUser: (updates: Partial<User>) => void;
}

// 3. Create context with undefined default
const UserContext = createContext<UserContextType | undefined>(undefined);

// 4. Provider props type
interface UserProviderProps {
    children: ReactNode;
}

// 5. Custom Provider component
export function UserProvider({ children }: UserProviderProps) {
    const [user, setUser] = useState<User | null>(null);

    const login = useCallback((newUser: User) => {
        setUser(newUser);
        localStorage.setItem('user', JSON.stringify(newUser));
    }, []);

    const logout = useCallback(() => {
        setUser(null);
        localStorage.removeItem('user');
    }, []);

    const updateUser = useCallback((updates: Partial<User>) => {
        setUser(current => {
            if (!current) return null;
            return { ...current, ...updates };
        });
    }, []);

    const value: UserContextType = {
        user,
        isLoggedIn: user !== null,
        login,
        logout,
        updateUser
    };

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

// 6. Custom hook with type guard
export function useUser(): UserContextType {
    const context = useContext(UserContext);
    
    if (context === undefined) {
        throw new Error('useUser must be used within UserProvider');
    }
    
    return context;
}

// 7. Optional: Additional utility hooks
export function useIsAdmin(): boolean {
    const { user } = useUser();
    return user?.role === 'admin';
}

export function useCurrentUserId(): number | null {
    const { user } = useUser();
    return user?.id ?? null;
}

πŸ’‘ TypeScript Features Used

  • Interface - Define shape of User and context
  • Union Types - User | null for optional user
  • Literal Types - 'admin' | 'user' for role
  • Partial Type - Partial<User> for updates
  • ReactNode - Type for children prop
  • Type Guard - Error throwing removes undefined

Generic Context Pattern

For reusable contexts, use generics:

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

// Generic state context creator
function createStateContext<T>(defaultValue: T) {
    const Context = createContext<{
        value: T;
        setValue: (value: T) => void;
    } | undefined>(undefined);

    function Provider({ children }: { children: ReactNode }) {
        const [value, setValue] = useState<T>(defaultValue);

        return (
            <Context.Provider value={{ value, setValue }}>
                {children}
            </Context.Provider>
        );
    }

    function useValue() {
        const context = useContext(Context);
        if (!context) {
            throw new Error('useValue must be used within Provider');
        }
        return context;
    }

    return [Provider, useValue] as const;
}

// Usage - create typed contexts easily!
const [CountProvider, useCount] = createStateContext(0);
const [NameProvider, useName] = createStateContext('');
const [UserProvider, useUser] = createStateContext<User | null>(null);

Typing Context Actions

When combining with useReducer, type your actions carefully:

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

// State type
interface Todo {
    id: number;
    text: string;
    completed: boolean;
}

interface TodoState {
    todos: Todo[];
}

// Action types
type TodoAction =
    | { type: 'ADD_TODO'; payload: { text: string } }
    | { type: 'TOGGLE_TODO'; payload: { id: number } }
    | { type: 'DELETE_TODO'; payload: { id: number } };

// Context type
interface TodoContextType {
    state: TodoState;
    dispatch: React.Dispatch<TodoAction>;
}

const TodoContext = createContext<TodoContextType | undefined>(undefined);

// Reducer
function todoReducer(state: TodoState, action: TodoAction): TodoState {
    // Reducer implementation
    return state;
}

// Provider
export function TodoProvider({ children }: { children: ReactNode }) {
    const [state, dispatch] = useReducer(todoReducer, { todos: [] });

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

// Custom hook
export function useTodos(): TodoContextType {
    const context = useContext(TodoContext);
    if (!context) {
        throw new Error('useTodos must be used within TodoProvider');
    }
    return context;
}

🎨 Theme Context Example

Let's build a complete, real-world example: a theme switcher that allows users to toggle between light and dark modes. This will demonstrate all the concepts we've learned so far.

Project Structure

We'll create a theme context with the following features:

Features We'll Build

  • πŸŒ“ Light and dark theme modes
  • πŸ”„ Toggle between themes
  • πŸ’Ύ Persist preference to localStorage
  • 🎨 Apply theme styles to entire app
  • πŸ”· Full TypeScript typing

Step 1: Define Types and Create Context

// src/contexts/ThemeContext.tsx
import { createContext, useContext } from 'react';

// Define theme type
export type Theme = 'light' | 'dark';

// Define context type
interface ThemeContextType {
    theme: Theme;
    toggleTheme: () => void;
    setTheme: (theme: Theme) => void;
}

// Create context
const ThemeContext = createContext<ThemeContextType | undefined>(undefined);

Step 2: Create the Provider

// Continue in ThemeContext.tsx
import { useState, useEffect, ReactNode } from 'react';

interface ThemeProviderProps {
    children: ReactNode;
}

export function ThemeProvider({ children }: ThemeProviderProps) {
    // Initialize theme from localStorage or default to 'light'
    const [theme, setTheme] = useState<Theme>(() => {
        const savedTheme = localStorage.getItem('theme');
        return (savedTheme as Theme) || 'light';
    });

    // Save theme to localStorage when it changes
    useEffect(() => {
        localStorage.setItem('theme', theme);
        // Apply theme to document for CSS
        document.documentElement.setAttribute('data-theme', theme);
    }, [theme]);

    // Toggle between light and dark
    const toggleTheme = () => {
        setTheme(prevTheme => prevTheme === 'light' ? 'dark' : 'light');
    };

    const value: ThemeContextType = {
        theme,
        toggleTheme,
        setTheme
    };

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

Step 3: Create Custom Hook

// Continue in ThemeContext.tsx

// Custom hook with error checking
export function useTheme(): ThemeContextType {
    const context = useContext(ThemeContext);
    
    if (context === undefined) {
        throw new Error('useTheme must be used within ThemeProvider');
    }
    
    return context;
}

// Complete ThemeContext.tsx file is now ready!

Step 4: Add CSS Styles

/* src/styles/theme.css */

/* Light theme (default) */
:root,
[data-theme="light"] {
    --bg-primary: #ffffff;
    --bg-secondary: #f5f5f5;
    --text-primary: #333333;
    --text-secondary: #666666;
    --border-color: #dddddd;
    --accent-color: #667eea;
    --card-bg: #ffffff;
    --shadow: rgba(0, 0, 0, 0.1);
}

/* Dark theme */
[data-theme="dark"] {
    --bg-primary: #1a1a1a;
    --bg-secondary: #2d2d2d;
    --text-primary: #e0e0e0;
    --text-secondary: #a0a0a0;
    --border-color: #404040;
    --accent-color: #8b9bff;
    --card-bg: #252525;
    --shadow: rgba(0, 0, 0, 0.5);
}

/* Apply theme variables */
body {
    background-color: var(--bg-primary);
    color: var(--text-primary);
    transition: background-color 0.3s ease, color 0.3s ease;
}

.card {
    background-color: var(--card-bg);
    border: 1px solid var(--border-color);
    box-shadow: 0 2px 8px var(--shadow);
}

.button {
    background-color: var(--accent-color);
    color: white;
}

Step 5: Wrap App with Provider

// src/main.tsx or App.tsx
import { ThemeProvider } from './contexts/ThemeContext';
import './styles/theme.css';

function App() {
    return (
        <ThemeProvider>
            <Header />
            <MainContent />
            <Footer />
        </ThemeProvider>
    );
}

export default App;

Step 6: Use Theme in Components

// src/components/Header.tsx
import { useTheme } from '../contexts/ThemeContext';

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

    return (
        <header>
            <h1>My App</h1>
            <button onClick={toggleTheme}>
                {theme === 'light' ? 'πŸŒ™' : 'β˜€οΈ'}
                {theme === 'light' ? ' Dark Mode' : ' Light Mode'}
            </button>
        </header>
    );
}

export default Header;

Step 7: Create Theme Toggle Component

// src/components/ThemeToggle.tsx
import { useTheme } from '../contexts/ThemeContext';
import './ThemeToggle.css';

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

    return (
        <button 
            className="theme-toggle"
            onClick={toggleTheme}
            aria-label="Toggle theme"
        >
            <span className="theme-toggle-icon">
                {theme === 'light' ? 'πŸŒ™' : 'β˜€οΈ'}
            </span>
            <span className="theme-toggle-text">
                {theme === 'light' ? 'Dark' : 'Light'}
            </span>
        </button>
    );
}

export default ThemeToggle;
/* ThemeToggle.css */
.theme-toggle {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    padding: 0.5rem 1rem;
    background-color: var(--bg-secondary);
    border: 1px solid var(--border-color);
    border-radius: 8px;
    cursor: pointer;
    transition: all 0.2s ease;
}

.theme-toggle:hover {
    background-color: var(--accent-color);
    color: white;
    transform: translateY(-2px);
}

.theme-toggle-icon {
    font-size: 1.2rem;
}

.theme-toggle-text {
    font-weight: 500;
}

Complete Example with Multiple Components

// src/components/Dashboard.tsx
import { useTheme } from '../contexts/ThemeContext';

function Dashboard() {
    const { theme } = useTheme();

    return (
        <div className="dashboard">
            <h1>Dashboard</h1>
            <p>Current theme: {theme}</p>
            
            <div className="card">
                <h2>Stats</h2>
                <p>This card uses theme variables for styling!</p>
            </div>

            <div className="card">
                <h2>Activity</h2>
                <p>The theme changes apply to all components automatically.</p>
            </div>
        </div>
    );
}

// src/components/Settings.tsx
function Settings() {
    const { theme, setTheme } = useTheme();

    return (
        <div className="settings">
            <h2>Settings</h2>
            
            <div className="setting-group">
                <label>Theme Preference:</label>
                <select 
                    value={theme} 
                    onChange={(e) => setTheme(e.target.value as 'light' | 'dark')}
                >
                    <option value="light">Light</option>
                    <option value="dark">Dark</option>
                </select>
            </div>
        </div>
    );
}

βœ… What We Accomplished

  • Context Creation - Defined theme context with TypeScript
  • Provider Setup - Created provider with localStorage persistence
  • Custom Hook - Made useTheme hook with error checking
  • CSS Variables - Used data attributes for theme styling
  • Multiple Components - Any component can access theme
  • Persistence - Theme survives page refresh
  • Smooth Transitions - CSS transitions for theme changes

Enhanced Theme with System Preference

Let's add detection of the user's system theme preference:

// Enhanced ThemeProvider
export function ThemeProvider({ children }: ThemeProviderProps) {
    // Get system preference
    const getSystemTheme = (): Theme => {
        if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
            return 'dark';
        }
        return 'light';
    };

    // Initialize with saved theme, or system preference, or default
    const [theme, setTheme] = useState<Theme>(() => {
        const savedTheme = localStorage.getItem('theme');
        if (savedTheme) {
            return savedTheme as Theme;
        }
        return getSystemTheme();
    });

    // Listen for system theme changes
    useEffect(() => {
        const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
        
        const handleChange = (e: MediaQueryListEvent) => {
            if (!localStorage.getItem('theme')) {
                // Only update if user hasn't set a preference
                setTheme(e.matches ? 'dark' : 'light');
            }
        };

        mediaQuery.addEventListener('change', handleChange);
        return () => mediaQuery.removeEventListener('change', handleChange);
    }, []);

    // Rest of the provider code...
}

πŸ’‘ Advanced Features Added

  • System Preference Detection - Uses prefers-color-scheme
  • Automatic Updates - Responds to system theme changes
  • User Preference Priority - Manual choice overrides system
  • Smart Defaults - Falls back gracefully

βš™οΈ Context with useReducer

Combining Context with useReducer creates a powerful state management solution. This pattern is perfect for complex state that needs to be accessed by many components. Let's build a todo application to see this in action!

Why Combine Context + useReducer?

The Power Combination

  • useReducer - Manages complex state logic in one place
  • Context - Makes state and dispatch available everywhere
  • Together - Global state management without Redux!
graph TD A[Context + useReducer] --> B[Complex State Logic] A --> C[Global Accessibility] B --> D[Predictable Updates] C --> D D --> E[Scalable State Management] style A fill:#667eea,stroke:#333,stroke-width:2px,color:#fff style E fill:#48bb78,stroke:#333,stroke-width:2px,color:#fff

Complete Todo Context Example

Step 1: Define Types

// src/contexts/TodoContext.tsx
import { createContext, useContext, useReducer, ReactNode } from 'react';

// Todo type
export interface Todo {
    id: number;
    text: string;
    completed: boolean;
}

// State type
interface TodoState {
    todos: Todo[];
    filter: 'all' | 'active' | 'completed';
}

// Action types
type TodoAction =
    | { type: 'ADD_TODO'; payload: { text: string } }
    | { type: 'TOGGLE_TODO'; payload: { id: number } }
    | { type: 'DELETE_TODO'; payload: { id: number } }
    | { type: 'EDIT_TODO'; payload: { id: number; text: string } }
    | { type: 'SET_FILTER'; payload: { filter: 'all' | 'active' | 'completed' } }
    | { type: 'CLEAR_COMPLETED' };

Step 2: Create Reducer

// Continue in TodoContext.tsx

function todoReducer(state: TodoState, action: TodoAction): TodoState {
    switch (action.type) {
        case 'ADD_TODO':
            return {
                ...state,
                todos: [
                    ...state.todos,
                    {
                        id: Date.now(),
                        text: action.payload.text,
                        completed: false
                    }
                ]
            };

        case 'TOGGLE_TODO':
            return {
                ...state,
                todos: state.todos.map(todo =>
                    todo.id === action.payload.id
                        ? { ...todo, completed: !todo.completed }
                        : todo
                )
            };

        case 'DELETE_TODO':
            return {
                ...state,
                todos: state.todos.filter(todo => todo.id !== action.payload.id)
            };

        case 'EDIT_TODO':
            return {
                ...state,
                todos: state.todos.map(todo =>
                    todo.id === action.payload.id
                        ? { ...todo, text: action.payload.text }
                        : todo
                )
            };

        case 'SET_FILTER':
            return {
                ...state,
                filter: action.payload.filter
            };

        case 'CLEAR_COMPLETED':
            return {
                ...state,
                todos: state.todos.filter(todo => !todo.completed)
            };

        default:
            return state;
    }
}

Step 3: Create Context and Provider

// Continue in TodoContext.tsx

// Context type
interface TodoContextType {
    state: TodoState;
    dispatch: React.Dispatch<TodoAction>;
}

// Create context
const TodoContext = createContext<TodoContextType | undefined>(undefined);

// Provider component
export function TodoProvider({ children }: { children: ReactNode }) {
    const [state, dispatch] = useReducer(todoReducer, {
        todos: [],
        filter: 'all'
    });

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

// Custom hook
export function useTodos() {
    const context = useContext(TodoContext);
    
    if (context === undefined) {
        throw new Error('useTodos must be used within TodoProvider');
    }
    
    return context;
}

Step 4: Create Action Creators

// Continue in TodoContext.tsx

// Action creators for cleaner usage
export const todoActions = {
    addTodo: (text: string): TodoAction => ({
        type: 'ADD_TODO',
        payload: { text }
    }),

    toggleTodo: (id: number): TodoAction => ({
        type: 'TOGGLE_TODO',
        payload: { id }
    }),

    deleteTodo: (id: number): TodoAction => ({
        type: 'DELETE_TODO',
        payload: { id }
    }),

    editTodo: (id: number, text: string): TodoAction => ({
        type: 'EDIT_TODO',
        payload: { id, text }
    }),

    setFilter: (filter: 'all' | 'active' | 'completed'): TodoAction => ({
        type: 'SET_FILTER',
        payload: { filter }
    }),

    clearCompleted: (): TodoAction => ({
        type: 'CLEAR_COMPLETED'
    })
};

Step 5: Use in Components

// src/components/AddTodo.tsx
import { useState } from 'react';
import { useTodos, todoActions } from '../contexts/TodoContext';

function AddTodo() {
    const { dispatch } = useTodos();
    const [text, setText] = useState('');

    const handleSubmit = (e: React.FormEvent) => {
        e.preventDefault();
        if (text.trim()) {
            dispatch(todoActions.addTodo(text));
            setText('');
        }
    };

    return (
        <form onSubmit={handleSubmit}>
            <input
                type="text"
                value={text}
                onChange={(e) => setText(e.target.value)}
                placeholder="What needs to be done?"
            />
            <button type="submit">Add</button>
        </form>
    );
}

export default AddTodo;
// src/components/TodoList.tsx
import { useTodos, todoActions } from '../contexts/TodoContext';
import TodoItem from './TodoItem';

function TodoList() {
    const { state, dispatch } = useTodos();

    // Filter todos based on current filter
    const filteredTodos = state.todos.filter(todo => {
        if (state.filter === 'active') return !todo.completed;
        if (state.filter === 'completed') return todo.completed;
        return true; // 'all'
    });

    return (
        <div>
            <ul className="todo-list">
                {filteredTodos.map(todo => (
                    <TodoItem key={todo.id} todo={todo} />
                ))}
            </ul>

            {filteredTodos.length === 0 && (
                <p className="empty-state">No todos to show</p>
            )}
        </div>
    );
}

export default TodoList;
// src/components/TodoItem.tsx
import { useTodos, todoActions, Todo } from '../contexts/TodoContext';

interface TodoItemProps {
    todo: Todo;
}

function TodoItem({ todo }: TodoItemProps) {
    const { dispatch } = useTodos();

    return (
        <li className={todo.completed ? 'completed' : ''}>
            <input
                type="checkbox"
                checked={todo.completed}
                onChange={() => dispatch(todoActions.toggleTodo(todo.id))}
            />
            <span>{todo.text}</span>
            <button onClick={() => dispatch(todoActions.deleteTodo(todo.id))}>
                Delete
            </button>
        </li>
    );
}

export default TodoItem;
// src/components/TodoFilters.tsx
import { useTodos, todoActions } from '../contexts/TodoContext';

function TodoFilters() {
    const { state, dispatch } = useTodos();

    const filters: Array<'all' | 'active' | 'completed'> = [
        'all',
        'active', 
        'completed'
    ];

    return (
        <div className="filters">
            {filters.map(filter => (
                <button
                    key={filter}
                    className={state.filter === filter ? 'active' : ''}
                    onClick={() => dispatch(todoActions.setFilter(filter))}
                >
                    {filter.charAt(0).toUpperCase() + filter.slice(1)}
                </button>
            ))}
        </div>
    );
}

export default TodoFilters;

Step 6: Put It All Together

// src/App.tsx
import { TodoProvider } from './contexts/TodoContext';
import AddTodo from './components/AddTodo';
import TodoList from './components/TodoList';
import TodoFilters from './components/TodoFilters';

function App() {
    return (
        <TodoProvider>
            <div className="app">
                <h1>πŸ“ Todo App</h1>
                <AddTodo />
                <TodoFilters />
                <TodoList />
            </div>
        </TodoProvider>
    );
}

export default App;

βœ… What We Achieved

  • Global State - All components access the same todo state
  • Predictable Updates - Reducer ensures consistent state changes
  • Type Safety - TypeScript prevents action type errors
  • Clean Components - Components focus on UI, not logic
  • No Prop Drilling - Deep components access state directly
  • Testability - Reducer can be tested independently
  • Scalability - Easy to add new actions and features

Optimized Context Pattern

For better performance, split state and dispatch into separate contexts:

// Advanced pattern - separate contexts for state and dispatch
const TodoStateContext = createContext<TodoState | undefined>(undefined);
const TodoDispatchContext = createContext<React.Dispatch<TodoAction> | undefined>(undefined);

export function TodoProvider({ children }: { children: ReactNode }) {
    const [state, dispatch] = useReducer(todoReducer, initialState);

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

// Separate hooks
export function useTodoState() {
    const context = useContext(TodoStateContext);
    if (!context) throw new Error('useTodoState must be used within TodoProvider');
    return context;
}

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

// Now components only re-render when the value they use changes!
// Component that only dispatches doesn't re-render when state changes
function AddTodo() {
    const dispatch = useTodoDispatch(); // Won't re-render on state change
    // ...
}

// Component that only reads state doesn't need dispatch
function TodoStats() {
    const state = useTodoState(); // Clean and efficient
    // ...
}

πŸ’‘ Performance Benefit

Splitting contexts means components only re-render when the specific value they use changes. A component that only dispatches actions won't re-render when state changes, and vice versa. This is especially important for large applications!

πŸ‹οΈ Hands-on Practice

Now it's your turn! These exercises will help you master Context and useContext. Start with the easier ones and work your way up.

Exercise 1: Language Context

🎯 Challenge

Create a language/internationalization (i18n) context that allows users to switch between languages.

Requirements:

  • Support at least 2 languages (e.g., English and Spanish)
  • Provide a way to switch languages
  • Store translations in objects
  • Create a custom hook to access translations
  • Persist language preference to localStorage
πŸ’‘ Hint

Your context might look like:

interface LanguageContextType {
    language: 'en' | 'es';
    setLanguage: (lang: 'en' | 'es') => void;
    t: (key: string) => string; // translation function
}

Translation object example:

const translations = {
    en: {
        welcome: 'Welcome',
        greeting: 'Hello, {name}!'
    },
    es: {
        welcome: 'Bienvenido',
        greeting: 'Β‘Hola, {name}!'
    }
};
βœ… Solution
// LanguageContext.tsx
import { createContext, useContext, useState, ReactNode } from 'react';

type Language = 'en' | 'es';

const translations = {
    en: {
        welcome: 'Welcome',
        login: 'Login',
        logout: 'Logout',
        settings: 'Settings'
    },
    es: {
        welcome: 'Bienvenido',
        login: 'Iniciar sesiΓ³n',
        logout: 'Cerrar sesiΓ³n',
        settings: 'ConfiguraciΓ³n'
    }
};

interface LanguageContextType {
    language: Language;
    setLanguage: (lang: Language) => void;
    t: (key: string) => string;
}

const LanguageContext = createContext<LanguageContextType | undefined>(undefined);

export function LanguageProvider({ children }: { children: ReactNode }) {
    const [language, setLanguage] = useState<Language>(() => {
        const saved = localStorage.getItem('language');
        return (saved as Language) || 'en';
    });

    const handleSetLanguage = (lang: Language) => {
        setLanguage(lang);
        localStorage.setItem('language', lang);
    };

    const t = (key: string): string => {
        return translations[language][key as keyof typeof translations.en] || key;
    };

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

export function useLanguage() {
    const context = useContext(LanguageContext);
    if (!context) throw new Error('useLanguage must be used within LanguageProvider');
    return context;
}

Exercise 2: Shopping Cart Context

🎯 Challenge

Build a shopping cart context using Context + useReducer.

Requirements:

  • Add items to cart (with quantity)
  • Remove items from cart
  • Update item quantities
  • Calculate total price
  • Clear entire cart
  • Use TypeScript for type safety
πŸ’‘ Hint

Types you'll need:

interface CartItem {
    id: string;
    name: string;
    price: number;
    quantity: number;
}

type CartAction =
    | { type: 'ADD_ITEM'; payload: CartItem }
    | { type: 'REMOVE_ITEM'; payload: { id: string } }
    | { type: 'UPDATE_QUANTITY'; payload: { id: string; quantity: number } }
    | { type: 'CLEAR_CART' };

Exercise 3: Authentication Context

🎯 Challenge

Create an authentication context with login, logout, and protected route logic.

Requirements:

  • Login/logout functionality
  • Store user information
  • Check authentication status
  • Persist auth state (localStorage or sessionStorage)
  • Provide loading state during authentication
  • Include user roles/permissions
πŸ’‘ Hint

Context structure:

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

βœ… Practice Tips

  • Start with type definitions first
  • Build the context and provider before the hook
  • Test with console.log before building UI
  • Add one feature at a time
  • Use TypeScript errors as guides
  • Don't forget error boundaries!

⚑ Performance Considerations

Context is powerful, but it can cause performance issues if not used carefully. Let's learn how to optimize Context usage.

The Re-render Problem

When Context value changes, ALL components that consume it will re-render:

πŸ”„ Interactive: Context Re-render Visualizer

See which components re-render when context values change

Context Change β†’ Consumer Re-renders UserContext.Provider Context State: user: { name: "Alice" } theme: "light" Header useContext(User) βœ” Sidebar useContext(User) βœ” ArticleContent No context used Footer No context used UserProfile useContext(User) βœ” Settings useContext(User) βœ” Navigation useContext(User) βœ” Re-render Count 0 components re-rendered Context Consumer Non-Consumer Re-rendering! Click a button to change context and watch re-renders

⚠️ Notice: ALL 5 consumers re-render when context changes, even if they only use part of the data! This is why splitting contexts or using selectors can improve performance.

// ❌ Problem: Creates new object every render
function App() {
    const [user, setUser] = useState(null);
    
    return (
        <UserContext.Provider value={{ user, setUser }}>
            <Dashboard />
        </UserContext.Provider>
    );
}

// Every render of App creates a new object
// Even if user hasn't changed, ALL consumers re-render!

⚠️ Why This Happens

JavaScript creates a new object on each render:

{ user, setUser } !== { user, setUser } // Always true!
// React sees a different object, assumes value changed

Solution 1: Memoize the Value

import { useMemo } from 'react';

function App() {
    const [user, setUser] = useState(null);
    
    // βœ… Value only changes when user changes
    const value = useMemo(
        () => ({ user, setUser }),
        [user] // Only recreate if user changes
    );
    
    return (
        <UserContext.Provider value={value}>
            <Dashboard />
        </UserContext.Provider>
    );
}

Solution 2: Split Contexts

Separate state and dispatch to minimize re-renders:

// Two separate contexts
const UserStateContext = createContext(null);
const UserDispatchContext = createContext(null);

function UserProvider({ children }) {
    const [user, setUser] = useState(null);
    
    // dispatch is stable, never changes
    return (
        <UserStateContext.Provider value={user}>
            <UserDispatchContext.Provider value={setUser}>
                {children}
            </UserDispatchContext.Provider>
        </UserStateContext.Provider>
    );
}

// Components that only dispatch don't re-render on state changes!
function UpdateButton() {
    const setUser = useContext(UserDispatchContext);
    // Won't re-render when user state changes
    return <button onClick={() => setUser(...)}>Update</button>;
}

Solution 3: Optimize with React.memo

import { memo } from 'react';

// Expensive component that uses context
const ExpensiveComponent = memo(function ExpensiveComponent() {
    const { someValue } = useMyContext();
    
    // Complex rendering logic
    return <div>{someValue}</div>;
});

// Now it only re-renders when props or context actually change

Solution 4: Limit Provider Scope

// ❌ Too broad - entire app re-renders
function App() {
    return (
        <ThemeProvider>
            <Dashboard />
            <Settings />
            <Profile />
        </ThemeProvider>
    );
}

// βœ… Scoped - only affected components
function App() {
    return (
        <div>
            <Dashboard /> {/* Doesn't use theme */}
            <ThemeProvider>
                <Settings /> {/* Uses theme */}
                <Profile /> {/* Uses theme */}
            </ThemeProvider>
        </div>
    );
}

Performance Best Practices

βœ… Do's

  • Use useMemo to stabilize context values
  • Split contexts when state and actions are independent
  • Wrap expensive components in React.memo
  • Keep Provider scope as narrow as possible
  • Use multiple small contexts instead of one large one
  • Derive computed values in components, not context

⚠️ Don'ts

  • Don't create new objects/arrays in Provider value
  • Don't put everything in one giant context
  • Don't wrap entire app if only few components need it
  • Don't use Context for frequently changing values
  • Don't optimize prematurely - measure first!

When to Optimize

Scenario Action
Small app (<50 components) Don't worry about it
Context changes rarely No optimization needed
Few consumers Basic setup is fine
Large app (100+ components) Consider splitting contexts
Frequent context updates Use useMemo, split contexts
Performance issues detected Profile and optimize

⭐ Best Practices

Follow these best practices to write maintainable, scalable Context code.

βœ… Do: Create Custom Hooks

// βœ… Always provide a custom hook
export function useUser() {
    const context = useContext(UserContext);
    if (!context) {
        throw new Error('useUser must be used within UserProvider');
    }
    return context;
}

// Usage is clean and safe
function MyComponent() {
    const { user } = useUser(); // Clean!
}

βœ… Do: Organize Context Files

// src/contexts/UserContext.tsx
// Everything in one file:
// - Types
// - Context creation
// - Provider component
// - Custom hook
// - Export only what's needed

export { UserProvider, useUser };
// Don't export the context itself

βœ… Do: Use TypeScript

// βœ… Fully typed context
interface UserContextType {
    user: User | null;
    login: (credentials: LoginCredentials) => Promise<void>;
    logout: () => void;
}

const UserContext = createContext<UserContextType | undefined>(undefined);

βœ… Do: Split Large Contexts

// ❌ One massive context
interface AppContextType {
    user: User;
    theme: Theme;
    language: Language;
    cart: Cart;
    notifications: Notification[];
    // ... too much!
}

// βœ… Multiple focused contexts
// UserContext, ThemeContext, LanguageContext, CartContext, etc.
// Each handles one concern

βœ… Do: Provide Meaningful Errors

export function useUser() {
    const context = useContext(UserContext);
    if (!context) {
        throw new Error(
            'useUser must be used within UserProvider. ' +
            'Make sure your component is wrapped with <UserProvider>.'
        );
    }
    return context;
}

❌ Don't: Use Context for Everything

// ❌ Overkill for parent-child communication
function Parent() {
    return (
        <ValueContext.Provider value="hello">
            <Child />
        </ValueContext.Provider>
    );
}

// βœ… Just use props!
function Parent() {
    return <Child value="hello" />;
}

❌ Don't: Mutate Context Values

// ❌ Never mutate context values
const { user } = useUser();
user.name = 'New Name'; // NO!

// βœ… Use provided functions
const { user, updateUser } = useUser();
updateUser({ name: 'New Name' }); // YES!

Context Naming Conventions

Item Convention Example
Context PascalCase + Context UserContext
Provider PascalCase + Provider UserProvider
Custom Hook use + PascalCase useUser
Type Interface PascalCase + ContextType UserContextType

Testing Context

// Testing components that use context
import { render, screen } from '@testing-library/react';
import { UserProvider } from './UserContext';
import MyComponent from './MyComponent';

test('renders with user context', () => {
    render(
        <UserProvider>
            <MyComponent />
        </UserProvider>
    );
    
    expect(screen.getByText(/welcome/i)).toBeInTheDocument();
});

// Test with custom context value
test('renders with specific user', () => {
    const mockUser = { id: 1, name: 'Test User' };
    
    render(
        <UserContext.Provider value={{ user: mockUser, login: jest.fn(), logout: jest.fn() }}>
            <MyComponent />
        </UserContext.Provider>
    );
    
    expect(screen.getByText('Test User')).toBeInTheDocument();
});

πŸ“š Summary

Congratulations! You've mastered the useContext Hook and the Context API. Let's recap what you've learned.

🎯 Key Takeaways

  • Context solves prop drilling - Share data without passing props through every level
  • Three-part system - Create Context, Provide values, Consume with useContext
  • Custom hooks are essential - Always create custom hooks for type safety and error checking
  • TypeScript makes it better - Type safety prevents bugs and improves DX
  • Context + useReducer - Powerful pattern for complex global state
  • Performance matters - Use useMemo, split contexts, optimize strategically
  • Don't overuse - Props are fine for parent-child communication

What You Learned

Concept What You Can Do
Context Creation Create typed contexts with TypeScript
Providers Build custom Provider components with state
useContext Consume context values with the hook
Custom Hooks Create safe, reusable context hooks
Real-World Patterns Build theme switchers, auth, todos
Performance Optimize context for large applications

Common Use Cases for Context

  • 🎨 Theme - Dark/light mode, color schemes
  • πŸ” Authentication - Current user, login status
  • 🌐 Language - Internationalization (i18n)
  • πŸ›’ Shopping Cart - Cart items, totals
  • βš™οΈ Settings - User preferences
  • πŸ”” Notifications - Toast messages, alerts
  • πŸ“± Responsive - Screen size, device type
  • 🎯 Feature Flags - Enable/disable features

Context vs Other Solutions

Solution When to Use
Props Parent β†’ immediate child communication
Context Share data across many components at different nesting levels
Redux/Zustand Very large apps, time-travel debugging, complex middleware needs
React Query Server state management, caching, synchronization

Next Steps

πŸš€ Continue Learning

Now that you know useContext, you're ready for:

  • Lesson 5.3: useRef - Access DOM elements and store mutable values
  • Lesson 5.4: useMemo & useCallback - Performance optimization
  • Lesson 5.5: Compound Components - Advanced patterns
  • Module Project - Build a complete app with Context

Practice Projects

βœ… Build These to Master Context

  1. Multi-theme App - 3+ themes with custom colors
  2. E-commerce with Cart - Products, cart, checkout
  3. Dashboard with Auth - Login, protected routes, user management
  4. Kanban Board - Drag and drop with global state
  5. Multi-language Blog - i18n with language switching

πŸŽ‰ Congratulations! πŸŽ‰

You've completed the useContext lesson and gained a powerful tool for managing shared state. You can now build scalable React applications with clean, maintainable code!

Keep practicing, keep building, and keep learning! πŸš€