React.js Fundamentals

useContext & Context API - Global State Management

18 min Lesson 13 of 40

Understanding Context in React

Context provides a way to pass data through the component tree without having to pass props down manually at every level. It's designed to share data that can be considered "global" for a tree of React components, such as current authenticated user, theme, or preferred language.

The Context API consists of two main parts: a Provider component that supplies the data, and a way to consume that data in child components using either the useContext hook or a Consumer component.

When to Use Context

Use Context when you have data that needs to be accessible by many components at different nesting levels. Common use cases include: theming, user authentication, language preferences, and application-wide settings.

Creating a Context

First, create a context using React.createContext():

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

// Create context with default value
const ThemeContext = createContext('light');

export default ThemeContext;

Providing Context Value

Wrap your component tree with a Provider to make the context value available to all child components:

import React, { useState } from 'react';
import ThemeContext from './ThemeContext';

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

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

  return (
    <ThemeContext.Provider value={{ theme, toggleTheme }}>
      <div className={`app ${theme}`}>
        <Header />
        <MainContent />
        <Footer />
      </div>
    </ThemeContext.Provider>
  );
}

export default App;

Consuming Context with useContext

The useContext hook is the easiest way to access context values in function components:

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

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

  return (
    <header className={`header ${theme}`}>
      <h1>My Application</h1>
      <button onClick={toggleTheme}>
        Switch to {theme === 'light' ? 'Dark' : 'Light'} Mode
      </button>
    </header>
  );
}

export default Header;

Context Provider Pattern

Create a custom provider component that encapsulates context creation, state management, and provider logic. This keeps your code organized and makes the context easier to use.

Complete Theme Context Example

Here's a full example with a custom provider:

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

const ThemeContext = createContext();

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

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

  const value = {
    theme,
    toggleTheme
  };

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

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

// App.js
import React from 'react';
import { ThemeProvider } from './ThemeContext';
import Header from './Header';
import Content from './Content';

function App() {
  return (
    <ThemeProvider>
      <div className="app">
        <Header />
        <Content />
      </div>
    </ThemeProvider>
  );
}

export default App;

// Header.js
import React from 'react';
import { useTheme } from './ThemeContext';

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

  return (
    <header style={{
      background: theme === 'light' ? '#fff' : '#333',
      color: theme === 'light' ? '#000' : '#fff'
    }}>
      <h1>Theme Example</h1>
      <button onClick={toggleTheme}>
        Toggle Theme
      </button>
    </header>
  );
}

export default Header;

Authentication Context Example

A common use case is managing user authentication state:

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

const AuthContext = createContext();

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

  const login = async (email, password) => {
    setLoading(true);
    try {
      // Simulate API call
      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);
      localStorage.setItem('token', data.token);
    } catch (error) {
      console.error('Login failed:', error);
      throw error;
    } finally {
      setLoading(false);
    }
  };

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

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

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

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

// Usage in component
import React from 'react';
import { useAuth } from './AuthContext';

function UserProfile() {
  const { user, logout, isAuthenticated } = useAuth();

  if (!isAuthenticated) {
    return <div>Please log in</div>;
  }

  return (
    <div>
      <h2>Welcome, {user.name}!</h2>
      <p>Email: {user.email}</p>
      <button onClick={logout}>Logout</button>
    </div>
  );
}

Context Performance Considerations

When a context value changes, all components consuming that context will re-render. To optimize performance, split contexts by concern and use React.memo for expensive child components.

Multiple Contexts

You can nest multiple context providers to use different contexts:

import React from 'react';
import { ThemeProvider } from './ThemeContext';
import { AuthProvider } from './AuthContext';
import { LanguageProvider } from './LanguageContext';

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

// Component using multiple contexts
function Dashboard() {
  const { user } = useAuth();
  const { theme } = useTheme();
  const { language, t } = useLanguage();

  return (
    <div className={`dashboard ${theme}`}>
      <h1>{t('welcome')}, {user.name}!</h1>
      <p>Current language: {language}</p>
    </div>
  );
}

Optimizing Context Updates

Split context values to prevent unnecessary re-renders:

// Instead of one context with all user data
// Split into separate contexts for state and actions

// UserStateContext.js
const UserStateContext = createContext();
const UserActionsContext = createContext();

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

  // Actions don't change, so they won't cause re-renders
  const actions = useMemo(() => ({
    updateUser: (newUser) => setUser(newUser),
    clearUser: () => setUser(null)
  }), []);

  return (
    <UserStateContext.Provider value={user}>
      <UserActionsContext.Provider value={actions}>
        {children}
      </UserActionsContext.Provider>
    </UserStateContext.Provider>
  );
}

export function useUserState() {
  return useContext(UserStateContext);
}

export function useUserActions() {
  return useContext(UserActionsContext);
}

// Components only re-render when their specific context changes
function UserDisplay() {
  const user = useUserState(); // Re-renders when user changes
  return <div>{user?.name}</div>;
}

function UserControls() {
  const { updateUser } = useUserActions(); // Never re-renders
  return <button onClick={() => updateUser({ name: 'John' })}>Update</button>;
}

Language/Localization Context

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

const translations = {
  en: {
    welcome: 'Welcome',
    goodbye: 'Goodbye',
    hello: 'Hello'
  },
  ar: {
    welcome: 'مرحبا',
    goodbye: 'وداعا',
    hello: 'أهلا'
  }
};

const LanguageContext = createContext();

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

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

  const changeLanguage = (lang) => {
    setLanguage(lang);
    document.documentElement.lang = lang;
    document.documentElement.dir = lang === 'ar' ? 'rtl' : 'ltr';
  };

  const value = {
    language,
    changeLanguage,
    t
  };

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

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

// Usage
function Greeting() {
  const { t, language, changeLanguage } = useLanguage();

  return (
    <div>
      <h1>{t('hello')}</h1>
      <button onClick={() => changeLanguage('en')}>English</button>
      <button onClick={() => changeLanguage('ar')}>العربية</button>
    </div>
  );
}

Shopping Cart Context Example

// CartContext.js
import React, { createContext, useContext, useReducer } from 'react';

const CartContext = createContext();

function cartReducer(state, action) {
  switch (action.type) {
    case 'ADD_ITEM':
      const existingIndex = state.items.findIndex(
        item => item.id === action.payload.id
      );

      if (existingIndex >= 0) {
        const newItems = [...state.items];
        newItems[existingIndex].quantity += 1;
        return { ...state, items: newItems };
      }

      return {
        ...state,
        items: [...state.items, { ...action.payload, quantity: 1 }]
      };

    case 'REMOVE_ITEM':
      return {
        ...state,
        items: state.items.filter(item => item.id !== action.payload)
      };

    case 'UPDATE_QUANTITY':
      return {
        ...state,
        items: state.items.map(item =>
          item.id === action.payload.id
            ? { ...item, quantity: action.payload.quantity }
            : item
        )
      };

    case 'CLEAR_CART':
      return { ...state, items: [] };

    default:
      return state;
  }
}

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

  const addItem = (item) => {
    dispatch({ type: 'ADD_ITEM', payload: item });
  };

  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 value = {
    items: state.items,
    addItem,
    removeItem,
    updateQuantity,
    clearCart,
    total,
    itemCount: state.items.length
  };

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

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

// Usage in component
function CartSummary() {
  const { items, total, itemCount, removeItem } = useCart();

  return (
    <div>
      <h2>Shopping Cart ({itemCount} items)</h2>
      {items.map(item => (
        <div key={item.id}>
          <span>{item.name} x {item.quantity}</span>
          <span>${item.price * item.quantity}</span>
          <button onClick={() => removeItem(item.id)}>Remove</button>
        </div>
      ))}
      <h3>Total: ${total.toFixed(2)}</h3>
    </div>
  );
}

Exercise 1: Dark Mode Context

Create a complete dark mode system with context that persists theme preference to localStorage.

// Requirements:
// 1. Create ThemeContext with light/dark modes
// 2. Save theme preference to localStorage
// 3. Apply theme class to body element
// 4. Create toggle button component
// 5. Provide custom useTheme hook

Exercise 2: Multi-Language App

Build a language context that supports English, Arabic, and French with translation function.

// Requirements:
// 1. Create LanguageContext with translations object
// 2. Provide t() function for translations
// 3. Change document direction for RTL languages
// 4. Language selector component
// 5. At least 10 translated keys

Exercise 3: Notification System

Create a notification context that manages success/error/info messages with auto-dismiss.

// Requirements:
// 1. NotificationContext with array of notifications
// 2. Methods: addNotification, removeNotification, clearAll
// 3. Auto-dismiss after 3 seconds
// 4. Support types: success, error, info, warning
// 5. NotificationList component to display all

Summary

  • Context provides a way to share data across component tree without prop drilling
  • Create context with createContext(), provide with Provider, consume with useContext
  • Custom provider patterns encapsulate context logic and state management
  • Custom hooks like useAuth() provide cleaner API and error checking
  • Split contexts by concern to optimize performance
  • Common use cases: theming, authentication, language, shopping cart
  • Context updates cause all consumers to re-render - optimize when needed