Context API and Global State

Module 22: Web Frameworks I (JavaScript) - Thursday: React Advanced

Understanding React Context

React Context provides a way to share data between components without having to explicitly pass props through every level of the component tree. This is especially useful for data that can be considered "global" for a tree of React components.

graph TD A[App] --> B[ThemeProvider] B --> C[Header] B --> D[Main Content] B --> E[Footer] D --> F[Sidebar] D --> G[Content Area] G --> H[Article] G --> I[Comments] J[Theme Context] -.-> B J -.-> C J -.-> E J -.-> F J -.-> H style J fill:#f9d71c,stroke:#333,stroke-width:2px style B fill:#a1ffa8,stroke:#333,stroke-width:2px

The Broadcasting Station Analogy

Think of React Context like a radio broadcasting system:

  • Context Provider: Acts as the radio station, broadcasting data to any component that's tuned in
  • Context Consumer or useContext: Acts as a radio receiver, allowing components to "tune in" and receive the data
  • Context Value: Represents the broadcast content, which can change over time
  • Default Value: Like a static-filled fallback that receivers use if they can't find the station

Just as multiple radio receivers can tune into the same station without being directly connected to it or to each other, multiple components can access the same context without being directly connected through props. And just like changing what's being broadcast affects all tuned-in receivers simultaneously, updating a context value affects all consuming components at once.

When to Use Context

Context is designed to share data that can be considered "global" for a tree of React components. Common use cases include:

The Problem Context Solves: Prop Drilling

Before Context, React developers faced a common issue called "prop drilling" - passing props through multiple component layers, even when intermediate components don't use the props themselves. This leads to several problems:

graph TD A[App with user data] --> B[Layout] B --> C[Header] C --> D[Navigation] D --> E[UserMenu] E --> F[UserProfile needs user data] style A fill:#f9d71c,stroke:#333,stroke-width:2px style F fill:#a1ffa8,stroke:#333,stroke-width:2px

Problems with Prop Drilling:

  • Verbose Code: Components need to accept and pass along props they don't use
  • Tight Coupling: Changes to data requirements cascade through the component tree
  • Refactoring Difficulty: Moving components requires rewiring prop chains
  • Component Reusability: Components become less reusable when tied to specific prop structures

Real-World Example:


// Before Context: Prop Drilling
function App() {
  const [user, setUser] = useState({
    name: 'John Doe',
    avatar: '/john.jpg',
    role: 'admin'
  });
  
  return (
    <Layout user={user} />
  );
}

function Layout({ user }) {
  return (
    <div className="layout">
      <Header user={user} />
      <MainContent />
      <Footer />
    </div>
  );
}

function Header({ user }) {
  return (
    <header>
      <Logo />
      <Navigation user={user} />
    </header>
  );
}

function Navigation({ user }) {
  return (
    <nav>
      <NavLinks />
      <UserMenu user={user} />
    </nav>
  );
}

function UserMenu({ user }) {
  return (
    <div className="user-menu">
      <img src={user.avatar} alt={user.name} />
      <span>{user.name}</span>
      {user.role === 'admin' && <AdminPanel />}
    </div>
  );
}
                

In many real applications, prop drilling becomes even more complex with deeper component trees and more data being passed around. Context provides a cleaner solution by allowing data to be accessed directly where it's needed.

Creating and Using Context

Let's explore how to create and use Context in React applications, starting with the basic API and moving to more advanced patterns.

Basic Context Creation and Usage


// 1. Create a Context
import React, { createContext, useContext, useState } from 'react';

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

// 2. Create a Provider Component
function ThemeProvider({ children }) {
  const [theme, setTheme] = useState('light');
  
  // Toggle theme function
  const toggleTheme = () => {
    setTheme(prevTheme => prevTheme === 'light' ? 'dark' : 'light');
  };
  
  // The value that will be provided to consumers
  const value = {
    theme,
    toggleTheme
  };
  
  return (
    <ThemeContext.Provider value={value}>
      {children}
    </ThemeContext.Provider>
  );
}

// 3. Create a custom hook for consuming the context
function useTheme() {
  const context = useContext(ThemeContext);
  
  if (context === undefined) {
    throw new Error('useTheme must be used within a ThemeProvider');
  }
  
  return context;
}

// 4. Using the Context in components
function App() {
  return (
    <ThemeProvider>
      <Header />
      <MainContent />
      <Footer />
    </ThemeProvider>
  );
}

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

function MainContent() {
  // This component doesn't need the theme
  return (
    <main>
      <Content />
    </main>
  );
}

function Content() {
  const { theme } = useTheme();
  
  return (
    <div className={`content ${theme}`}>
      <p>This content is using the {theme} theme.</p>
    </div>
  );
}

function Footer() {
  const { theme } = useTheme();
  
  return (
    <footer className={`footer ${theme}`}>
      <p>© 2023 My App</p>
    </footer>
  );
}

export { ThemeProvider, useTheme };
                

This example demonstrates the basic pattern for creating and using Context in React. Note how the MainContent component doesn't use the theme but its child (Content) can still access it directly through the Context, avoiding prop drilling.

Using Multiple Contexts


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

const UserContext = createContext();

function UserProvider({ children }) {
  const [user, setUser] = useState(null);
  
  const login = (userData) => {
    setUser(userData);
  };
  
  const logout = () => {
    setUser(null);
  };
  
  const value = {
    user,
    login,
    logout,
    isAuthenticated: !!user
  };
  
  return (
    <UserContext.Provider value={value}>
      {children}
    </UserContext.Provider>
  );
}

function useUser() {
  const context = useContext(UserContext);
  
  if (context === undefined) {
    throw new Error('useUser must be used within a UserProvider');
  }
  
  return context;
}

export { UserProvider, useUser };

// App.js
import React from 'react';
import { ThemeProvider } from './ThemeContext';
import { UserProvider } from './UserContext';
import Dashboard from './Dashboard';
import LoginPage from './LoginPage';

function App() {
  return (
    // Nesting contexts is perfectly fine
    <ThemeProvider>
      <UserProvider>
        <AppContent />
      </UserProvider>
    </ThemeProvider>
  );
}

function AppContent() {
  const { isAuthenticated } = useUser();
  
  return (
    <div className="app">
      {isAuthenticated ? <Dashboard /> : <LoginPage />}
    </div>
  );
}

// LoginPage.js
function LoginPage() {
  const { login } = useUser();
  const { theme } = useTheme();
  
  const handleLogin = () => {
    // In a real app, you'd validate credentials
    login({
      id: 1,
      name: 'John Doe',
      email: 'john@example.com',
      role: 'admin'
    });
  };
  
  return (
    <div className={`login-page ${theme}`}>
      <h2>Login</h2>
      {/* Login form would go here */}
      <button onClick={handleLogin}>Login as John</button>
    </div>
  );
}

// UserProfile.js
function UserProfile() {
  const { user, logout } = useUser();
  const { theme } = useTheme();
  
  if (!user) return null;
  
  return (
    <div className={`user-profile ${theme}`}>
      <h2>{user.name}</h2>
      <p>Email: {user.email}</p>
      <p>Role: {user.role}</p>
      <button onClick={logout}>Logout</button>
    </div>
  );
}
                

This example shows how to use multiple contexts together. Components can consume as many contexts as they need, and contexts can be nested to provide hierarchical data.

Context with Reducer for Complex State


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

// Define initial state
const initialState = {
  theme: 'light',
  user: null,
  notifications: [],
  sidebarOpen: false
};

// Create reducer function
function appReducer(state, action) {
  switch (action.type) {
    case 'TOGGLE_THEME':
      return {
        ...state,
        theme: state.theme === 'light' ? 'dark' : 'light'
      };
    case 'LOGIN':
      return {
        ...state,
        user: action.payload
      };
    case 'LOGOUT':
      return {
        ...state,
        user: null
      };
    case 'ADD_NOTIFICATION':
      return {
        ...state,
        notifications: [...state.notifications, action.payload]
      };
    case 'REMOVE_NOTIFICATION':
      return {
        ...state,
        notifications: state.notifications.filter(
          notification => notification.id !== action.payload
        )
      };
    case 'TOGGLE_SIDEBAR':
      return {
        ...state,
        sidebarOpen: !state.sidebarOpen
      };
    default:
      throw new Error(`Unhandled action type: ${action.type}`);
  }
}

// Create context
const AppContext = createContext();

// Create provider
function AppProvider({ children }) {
  const [state, dispatch] = useReducer(appReducer, initialState);
  
  // Create convenience methods for common actions
  const value = {
    ...state,
    dispatch,
    toggleTheme: () => dispatch({ type: 'TOGGLE_THEME' }),
    login: (userData) => dispatch({ type: 'LOGIN', payload: userData }),
    logout: () => dispatch({ type: 'LOGOUT' }),
    addNotification: (notification) => {
      const id = Date.now();
      dispatch({
        type: 'ADD_NOTIFICATION',
        payload: { ...notification, id }
      });
      
      // Auto-remove notification after a delay
      setTimeout(() => {
        dispatch({ type: 'REMOVE_NOTIFICATION', payload: id });
      }, notification.duration || 3000);
    },
    toggleSidebar: () => dispatch({ type: 'TOGGLE_SIDEBAR' })
  };
  
  return (
    <AppContext.Provider value={value}>
      {children}
    </AppContext.Provider>
  );
}

// Create hook for using the context
function useApp() {
  const context = useContext(AppContext);
  
  if (context === undefined) {
    throw new Error('useApp must be used within an AppProvider');
  }
  
  return context;
}

export { AppProvider, useApp };

// Usage in components
function Header() {
  const { theme, toggleTheme, user, toggleSidebar } = useApp();
  
  return (
    <header className={`header ${theme}`}>
      <button onClick={toggleSidebar}>
        ☰
      </button>
      <h1>My App</h1>
      
      <div className="header-actions">
        {user ? (
          <span>Welcome, {user.name}</span>
        ) : (
          <button>Login</button>
        )}
        
        <button onClick={toggleTheme}>
          {theme === 'light' ? '🌙' : '☀️'}
        </button>
      </div>
    </header>
  );
}

function NotificationCenter() {
  const { notifications } = useApp();
  
  return (
    <div className="notifications">
      {notifications.map(notification => (
        <div
          key={notification.id}
          className={`notification ${notification.type || 'info'}`}
        >
          {notification.message}
        </div>
      ))}
    </div>
  );
}

function FeatureComponent() {
  const { addNotification } = useApp();
  
  const handleAction = () => {
    // Do something
    addNotification({
      message: 'Action completed successfully!',
      type: 'success',
      duration: 5000
    });
  };
  
  return (
    <div>
      <button onClick={handleAction}>Perform Action</button>
    </div>
  );
}
                

This example demonstrates using Context with useReducer for more complex state management. This approach is similar to Redux but contained within React's built-in API. It's particularly useful for applications with multiple related state transitions and complex logic.

Context Performance Considerations

While Context is powerful, it does have some performance implications that should be considered in real-world applications:

1. Context Re-renders

When a context value changes, all components that use that context will re-render, even if they only use a portion of the context value. Consider this scenario:


// This context combines user and theme data
const AppContext = createContext();

function AppProvider({ children }) {
  const [user, setUser] = useState(null);
  const [theme, setTheme] = useState('light');
  
  // Every time either user or theme changes, ALL components
  // using this context will re-render
  const value = { user, setUser, theme, setTheme };
  
  return (
    <AppContext.Provider value={value}>
      {children}
    </AppContext.Provider>
  );
}

// Even though this component only uses theme data,
// it will re-render when user data changes
function ThemedButton() {
  const { theme } = useContext(AppContext);
  console.log("ThemedButton rendering");
  
  return (
    <button className={theme}>Click Me</button>
  );
}
                

Solutions to Context Performance Issues:

A. Split Contexts by Domain

// Separate contexts for different data domains
const UserContext = createContext();
const ThemeContext = createContext();

function AppProvider({ children }) {
  return (
    <UserProvider>
      <ThemeProvider>
        {children}
      </ThemeProvider>
    </UserProvider>
  );
}

// Now this component only re-renders when theme changes
function ThemedButton() {
  const { theme } = useContext(ThemeContext);
  console.log("ThemedButton rendering");
  
  return (
    <button className={theme}>Click Me</button>
  );
}
                
B. Use Context Selectors with useMemo

// With a single context but memoized components
function ThemedButton() {
  const { theme } = useContext(AppContext);
  console.log("ThemedButton rendering");
  
  // Memoize the component based on theme
  return useMemo(() => {
    console.log("ThemedButton internal render");
    return (
      <button className={theme}>Click Me</button>
    );
  }, [theme]); // Only re-render when theme changes
}
                
C. Value Memoization

function AppProvider({ children }) {
  const [user, setUser] = useState(null);
  const [theme, setTheme] = useState('light');
  
  // Memoize the context value to prevent unnecessary re-renders
  const value = useMemo(() => {
    return { user, setUser, theme, setTheme };
  }, [user, theme]);
  
  return (
    <AppContext.Provider value={value}>
      {children}
    </AppContext.Provider>
  );
}
                

In real-world applications, companies like Airbnb and Facebook often implement custom context solutions that incorporate these performance optimizations. Some teams create sophisticated context systems with selectors and memoization built in, providing Redux-like functionality with a more React-native feel.

Advanced Context Patterns

In production React applications, developers have established several advanced patterns for working with Context more effectively.

Context Module Pattern

This pattern encapsulates all the context-related code in a single module, making it easy to import and use.


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

// Define action types as constants
const ActionTypes = {
  LOGIN: 'user/login',
  LOGOUT: 'user/logout',
  UPDATE_PROFILE: 'user/updateProfile',
};

// Initial state
const initialState = {
  user: null,
  isAuthenticated: false,
  isLoading: false,
  error: null,
};

// Reducer function
function userReducer(state, action) {
  switch (action.type) {
    case ActionTypes.LOGIN:
      return {
        ...state,
        user: action.payload,
        isAuthenticated: true,
        error: null,
      };
    case ActionTypes.LOGOUT:
      return {
        ...state,
        user: null,
        isAuthenticated: false,
      };
    case ActionTypes.UPDATE_PROFILE:
      return {
        ...state,
        user: { ...state.user, ...action.payload },
      };
    default:
      throw new Error(`Unhandled action type: ${action.type}`);
  }
}

// Create context
const UserContext = createContext();

// Create provider component
function UserProvider({ children }) {
  const [state, dispatch] = useReducer(userReducer, initialState);
  
  // Create action creators
  const login = (userData) => {
    dispatch({ type: ActionTypes.LOGIN, payload: userData });
  };
  
  const logout = () => {
    dispatch({ type: ActionTypes.LOGOUT });
  };
  
  const updateProfile = (profileData) => {
    dispatch({ type: ActionTypes.UPDATE_PROFILE, payload: profileData });
  };
  
  // Memoize value to prevent unnecessary re-renders
  const value = React.useMemo(() => {
    return {
      ...state,
      login,
      logout,
      updateProfile,
    };
  }, [state]);
  
  return (
    <UserContext.Provider value={value}>
      {children}
    </UserContext.Provider>
  );
}

// Hook for consuming context
function useUser() {
  const context = useContext(UserContext);
  if (context === undefined) {
    throw new Error('useUser must be used within a UserProvider');
  }
  return context;
}

// Export everything needed to use this context
export { UserProvider, useUser, ActionTypes };

// Usage example:
// import { UserProvider, useUser } from './userContext';
                

This pattern provides a clean, encapsulated API for context usage throughout your application. It's similar to how Redux modules are organized, with actions, reducers, and selectors all in one place.

Async Operations with Context

Context can be combined with async operations for data fetching, API calls, etc.


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

const initialState = {
  data: null,
  isLoading: false,
  error: null,
};

function dataReducer(state, action) {
  switch (action.type) {
    case 'FETCH_START':
      return {
        ...state,
        isLoading: true,
        error: null,
      };
    case 'FETCH_SUCCESS':
      return {
        data: action.payload,
        isLoading: false,
        error: null,
      };
    case 'FETCH_ERROR':
      return {
        ...state,
        isLoading: false,
        error: action.payload,
      };
    default:
      throw new Error(`Unhandled action type: ${action.type}`);
  }
}

const DataContext = createContext();

function DataProvider({ children }) {
  const [state, dispatch] = useReducer(dataReducer, initialState);
  
  const fetchData = useCallback(async (url) => {
    dispatch({ type: 'FETCH_START' });
    
    try {
      const response = await fetch(url);
      
      if (!response.ok) {
        throw new Error(`HTTP error! Status: ${response.status}`);
      }
      
      const data = await response.json();
      dispatch({ type: 'FETCH_SUCCESS', payload: data });
      return data;
    } catch (error) {
      dispatch({ type: 'FETCH_ERROR', payload: error.message });
      throw error;
    }
  }, []);
  
  const value = {
    ...state,
    fetchData,
  };
  
  return (
    <DataContext.Provider value={value}>
      {children}
    </DataContext.Provider>
  );
}

function useData() {
  const context = useContext(DataContext);
  if (context === undefined) {
    throw new Error('useData must be used within a DataProvider');
  }
  return context;
}

export { DataProvider, useData };

// Usage in a component
function ProductList() {
  const { data, isLoading, error, fetchData } = useData();
  
  React.useEffect(() => {
    fetchData('/api/products');
  }, [fetchData]);
  
  if (isLoading) return <div>Loading...</div>;
  if (error) return <div>Error: {error}</div>;
  if (!data) return <div>No data</div>;
  
  return (
    <div>
      <h2>Products</h2>
      <ul>
        {data.map(product => (
          <li key={product.id}>{product.name} - ${product.price}</li>
        ))}
      </ul>
    </div>
  );
}
                

This pattern combines context with async operations, providing a clean way to handle loading states, errors, and data throughout your application.

Composing Multiple Contexts

For complex applications, you might have multiple contexts that need to work together.


// AppProviders.js - Combining multiple context providers
import React from 'react';
import { ThemeProvider } from './themeContext';
import { UserProvider } from './userContext';
import { DataProvider } from './dataContext';
import { NotificationProvider } from './notificationContext';

// Create a component that composes all providers
function AppProviders({ children }) {
  return (
    <ThemeProvider>
      <UserProvider>
        <DataProvider>
          <NotificationProvider>
            {children}
          </NotificationProvider>
        </DataProvider>
      </UserProvider>
    </ThemeProvider>
  );
}

export default AppProviders;

// App.js
import React from 'react';
import AppProviders from './AppProviders';
import MainApp from './MainApp';

function App() {
  return (
    <AppProviders>
      <MainApp />
    </AppProviders>
  );
}

// A component using multiple contexts
function ProfileSettings() {
  const { user, updateProfile } = useUser();
  const { theme, toggleTheme } = useTheme();
  const { addNotification } = useNotification();
  
  const handleSaveProfile = async (profileData) => {
    try {
      await updateProfile(profileData);
      addNotification({
        type: 'success',
        message: 'Profile updated successfully!',
      });
    } catch (error) {
      addNotification({
        type: 'error',
        message: `Failed to update profile: ${error.message}`,
      });
    }
  };
  
  return (
    <div className={`profile-settings ${theme}`}>
      <h2>Profile Settings for {user.name}</h2>
      
      <div className="setting-group">
        <h3>Appearance</h3>
        <label>
          Theme:
          <select 
            value={theme} 
            onChange={e => toggleTheme(e.target.value)}
          >
            <option value="light">Light</option>
            <option value="dark">Dark</option>
          </select>
        </label>
      </div>
      
      {/* Profile form fields would go here */}
      
      <button onClick={() => handleSaveProfile({ /* form data */ })}>
        Save Changes
      </button>
    </div>
  );
}
                

This pattern allows for a clean separation of concerns, with each context handling its own domain while components can easily access multiple contexts as needed.

Context with TypeScript

Adding TypeScript provides type safety to your context code.


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

// Define types
type Theme = 'light' | 'dark';

interface ThemeContextType {
  theme: Theme;
  toggleTheme: () => void;
  setTheme: (theme: Theme) => void;
}

// Create context with a default value
const ThemeContext = createContext<ThemeContextType | undefined>(undefined);

interface ThemeProviderProps {
  children: ReactNode;
  initialTheme?: Theme;
}

function ThemeProvider({ 
  children, 
  initialTheme = 'light'
}: ThemeProviderProps) {
  const [theme, setTheme] = useState<Theme>(initialTheme);
  
  const toggleTheme = () => {
    setTheme(prevTheme => (prevTheme === 'light' ? 'dark' : 'light'));
  };
  
  // Memoize the value to prevent unnecessary re-renders
  const value = React.useMemo(() => {
    return { theme, toggleTheme, setTheme };
  }, [theme]);
  
  return (
    <ThemeContext.Provider value={value}>
      {children}
    </ThemeContext.Provider>
  );
}

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

export { ThemeProvider, useTheme, type Theme, type ThemeContextType };

// Usage in a component
import React from 'react';
import { useTheme } from './themeContext';

interface ButtonProps {
  children: ReactNode;
  onClick?: () => void;
}

function ThemedButton({ children, onClick }: ButtonProps) {
  const { theme } = useTheme();
  
  return (
    <button 
      className={`btn btn-${theme}`}
      onClick={onClick}
    >
      {children}
    </button>
  );
}
                

TypeScript enhances context usage by providing type safety, which helps catch errors during development and improves the developer experience with better autocomplete and documentation.

Context vs. State Management Libraries

In real-world applications, teams must decide whether to use Context or a dedicated state management library like Redux, MobX, Zustand, or Recoil. Here's a comparison to help with that decision:

Feature React Context + useReducer Redux MobX Zustand
Learning Curve Low (part of React) Steep (new concepts) Moderate Low
Boilerplate Moderate High Low Very Low
Performance Good (needs optimization) Excellent Excellent Excellent
DevTools Basic React DevTools Excellent Good Good
Middleware Custom Implementation Built-in Custom Simple API
Time Travel No Yes With add-ons With add-ons
Serialization Manual Built-in Challenging Simple

When to Use Context:

  • Small to Medium Applications: When state management needs are simpler
  • UI State: For theme, layout preferences, UI visibility state
  • Authentication: User auth state that affects multiple components
  • App Settings: Configuration, language preferences, etc.
  • When You Want to Avoid External Dependencies: Using only React built-ins

When to Consider External Libraries:

  • Complex State Logic: When state transitions are complex or numerous
  • Performance Critical Apps: When fine-grained control over re-renders is necessary
  • Large Team Projects: When strict patterns and developer tooling are valuable
  • Complex Async Flows: When you need sophisticated handling of async operations
  • Extensive Debugging Needs: When time-travel debugging would be valuable

Real Company Examples:

  • Facebook: Uses a mix of Context and custom state management solutions
  • Airbnb: Uses Redux for global state and Context for UI state
  • Atlassian: Uses Context API for many features in their products
  • Shopify: Uses a combination of Redux and Context based on feature needs

Many companies are moving towards a hybrid approach, using Context for UI state and component-specific state, while using more robust state management libraries for business logic and data that affects the entire application.

Common Context Use Cases

Let's explore some common real-world use cases for Context in React applications, with implementation examples.

Authentication Context


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

const AuthContext = createContext();

function AuthProvider({ children }) {
  const [user, setUser] = useState(null);
  const [isLoading, setIsLoading] = useState(true);
  
  // Initialize auth state on mount
  useEffect(() => {
    // Check for existing auth token in localStorage
    const token = localStorage.getItem('auth_token');
    
    if (token) {
      // Fetch user data from API using the token
      fetchUserData(token)
        .then(userData => {
          setUser(userData);
          setIsLoading(false);
        })
        .catch(() => {
          // Token might be invalid or expired
          localStorage.removeItem('auth_token');
          setIsLoading(false);
        });
    } else {
      setIsLoading(false);
    }
  }, []);
  
  // Function to fetch user data from API
  const fetchUserData = async (token) => {
    const response = await fetch('/api/me', {
      headers: {
        'Authorization': `Bearer ${token}`
      }
    });
    
    if (!response.ok) {
      throw new Error('Failed to fetch user data');
    }
    
    return response.json();
  };
  
  // Login function
  const login = async (email, password) => {
    try {
      const response = await fetch('/api/login', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json'
        },
        body: JSON.stringify({ email, password })
      });
      
      if (!response.ok) {
        throw new Error('Login failed');
      }
      
      const data = await response.json();
      localStorage.setItem('auth_token', data.token);
      
      // Set user data in state
      setUser(data.user);
      
      return data.user;
    } catch (error) {
      console.error('Login error:', error);
      throw error;
    }
  };
  
  // Logout function
  const logout = () => {
    localStorage.removeItem('auth_token');
    setUser(null);
  };
  
  // Register function
  const register = async (userData) => {
    try {
      const response = await fetch('/api/register', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json'
        },
        body: JSON.stringify(userData)
      });
      
      if (!response.ok) {
        throw new Error('Registration failed');
      }
      
      const data = await response.json();
      localStorage.setItem('auth_token', data.token);
      
      // Set user data in state
      setUser(data.user);
      
      return data.user;
    } catch (error) {
      console.error('Registration error:', error);
      throw error;
    }
  };
  
  const value = {
    user,
    isLoading,
    isAuthenticated: !!user,
    login,
    logout,
    register
  };
  
  return (
    <AuthContext.Provider value={value}>
      {children}
    </AuthContext.Provider>
  );
}

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

export { AuthProvider, useAuth };

// Usage in components
function LoginForm() {
  const { login } = useAuth();
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');
  const [error, setError] = useState('');
  
  const handleSubmit = async (e) => {
    e.preventDefault();
    setError('');
    
    try {
      await login(email, password);
      // Redirect or show success message
    } catch (err) {
      setError('Invalid email or password');
    }
  };
  
  return (
    <form onSubmit={handleSubmit}>
      {error && <div className="error">{error}</div>}
      
      <div className="form-group">
        <label htmlFor="email">Email</label>
        <input
          id="email"
          type="email"
          value={email}
          onChange={(e) => setEmail(e.target.value)}
          required
        />
      </div>
      
      <div className="form-group">
        <label htmlFor="password">Password</label>
        <input
          id="password"
          type="password"
          value={password}
          onChange={(e) => setPassword(e.target.value)}
          required
        />
      </div>
      
      <button type="submit">Login</button>
    </form>
  );
}

function PrivateRoute({ children }) {
  const { isAuthenticated, isLoading } = useAuth();
  const navigate = useNavigate();
  
  useEffect(() => {
    if (!isLoading && !isAuthenticated) {
      navigate('/login');
    }
  }, [isLoading, isAuthenticated, navigate]);
  
  if (isLoading) {
    return <div>Loading...</div>;
  }
  
  return isAuthenticated ? children : null;
}

// In your app
function App() {
  return (
    <AuthProvider>
      <Router>
        <Routes>
          <Route path="/login" element={<LoginPage />} />
          <Route path="/register" element={<RegisterPage />} />
          <Route
            path="/dashboard"
            element={
              <PrivateRoute>
                <Dashboard />
              </PrivateRoute>
            }
          />
          <Route path="/" element={<HomePage />} />
        </Routes>
      </Router>
    </AuthProvider>
  );
}
                

This authentication context example provides a complete solution for user authentication, including login, logout, registration, and protected routes. It's a common pattern used in many React applications to manage user authentication state.

Internationalization (i18n) Context


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

// Sample translations
const translations = {
  en: {
    greeting: 'Hello',
    welcome: 'Welcome to our app',
    profile: 'Profile',
    settings: 'Settings',
    logout: 'Logout',
    loading: 'Loading...',
    // Add more translations as needed
  },
  es: {
    greeting: 'Hola',
    welcome: 'Bienvenido a nuestra aplicación',
    profile: 'Perfil',
    settings: 'Configuración',
    logout: 'Cerrar sesión',
    loading: 'Cargando...',
  },
  fr: {
    greeting: 'Bonjour',
    welcome: 'Bienvenue sur notre application',
    profile: 'Profil',
    settings: 'Paramètres',
    logout: 'Déconnexion',
    loading: 'Chargement...',
  },
};

const I18nContext = createContext();

function I18nProvider({ children }) {
  // Initialize with browser language or default to English
  const [locale, setLocale] = useState(() => {
    const savedLocale = localStorage.getItem('locale');
    const browserLocale = navigator.language.split('-')[0];
    return savedLocale || (translations[browserLocale] ? browserLocale : 'en');
  });
  
  // Update localStorage when locale changes
  useEffect(() => {
    localStorage.setItem('locale', locale);
    // Optionally update document lang attribute
    document.documentElement.lang = locale;
  }, [locale]);
  
  // Function to get translated text
  const t = (key, params = {}) => {
    const translation = translations[locale]?.[key] || translations.en?.[key] || key;
    
    // Replace any parameters in the translation
    return Object.entries(params).reduce(
      (acc, [param, value]) => acc.replace(`{{${param}}}`, value),
      translation
    );
  };
  
  // Get available languages
  const availableLocales = Object.keys(translations);
  
  const value = {
    locale,
    setLocale,
    t,
    availableLocales,
  };
  
  return (
    <I18nContext.Provider value={value}>
      {children}
    </I18nContext.Provider>
  );
}

function useI18n() {
  const context = useContext(I18nContext);
  
  if (context === undefined) {
    throw new Error('useI18n must be used within an I18nProvider');
  }
  
  return context;
}

export { I18nProvider, useI18n };

// Usage in components
function Header() {
  const { t, locale, setLocale, availableLocales } = useI18n();
  
  return (
    <header>
      <h1>{t('greeting')}, User!</h1>
      <p>{t('welcome')}</p>
      
      <div className="language-selector">
        <label htmlFor="language-select">Language:</label>
        <select
          id="language-select"
          value={locale}
          onChange={(e) => setLocale(e.target.value)}
        >
          {availableLocales.map((loc) => (
            <option key={loc} value={loc}>
              {loc.toUpperCase()}
            </option>
          ))}
        </select>
      </div>
      
      <nav>
        <ul>
          <li><a href="/profile">{t('profile')}</a></li>
          <li><a href="/settings">{t('settings')}</a></li>
          <li><a href="/logout">{t('logout')}</a></li>
        </ul>
      </nav>
    </header>
  );
}

// Using with parameters
function WelcomeMessage({ username }) {
  const { t } = useI18n();
  
  return <h2>{t('welcomeUser', { username })}</h2>;
}
                

This internationalization context provides a way to manage and access translations throughout your application. It's a simplified example, but it demonstrates the core concepts of an i18n system using Context.

Shopping Cart Context


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

// Initial state
const initialState = {
  items: [],
  itemCount: 0,
  total: 0,
};

// Action types
const ActionTypes = {
  ADD_ITEM: 'cart/addItem',
  REMOVE_ITEM: 'cart/removeItem',
  UPDATE_QUANTITY: 'cart/updateQuantity',
  CLEAR_CART: 'cart/clearCart',
};

// Cart reducer
function cartReducer(state, action) {
  switch (action.type) {
    case ActionTypes.ADD_ITEM: {
      const newItem = action.payload;
      
      // Check if item already exists in cart
      const existingItemIndex = state.items.findIndex(
        item => item.id === newItem.id
      );
      
      if (existingItemIndex >= 0) {
        // Item exists, update quantity
        const newItems = [...state.items];
        newItems[existingItemIndex] = {
          ...newItems[existingItemIndex],
          quantity: newItems[existingItemIndex].quantity + (newItem.quantity || 1),
        };
        
        return {
          ...state,
          items: newItems,
          itemCount: state.itemCount + (newItem.quantity || 1),
          total: state.total + (newItem.price * (newItem.quantity || 1)),
        };
      } else {
        // Item doesn't exist, add new item
        const itemWithQuantity = {
          ...newItem,
          quantity: newItem.quantity || 1,
        };
        
        return {
          ...state,
          items: [...state.items, itemWithQuantity],
          itemCount: state.itemCount + itemWithQuantity.quantity,
          total: state.total + (newItem.price * itemWithQuantity.quantity),
        };
      }
    }
    
    case ActionTypes.REMOVE_ITEM: {
      const itemId = action.payload;
      const itemToRemove = state.items.find(item => item.id === itemId);
      
      if (!itemToRemove) return state;
      
      return {
        ...state,
        items: state.items.filter(item => item.id !== itemId),
        itemCount: state.itemCount - itemToRemove.quantity,
        total: state.total - (itemToRemove.price * itemToRemove.quantity),
      };
    }
    
    case ActionTypes.UPDATE_QUANTITY: {
      const { id, quantity } = action.payload;
      
      if (quantity <= 0) {
        // If quantity is 0 or negative, remove the item
        return cartReducer(state, { type: ActionTypes.REMOVE_ITEM, payload: id });
      }
      
      const existingItemIndex = state.items.findIndex(item => item.id === id);
      
      if (existingItemIndex === -1) return state;
      
      const item = state.items[existingItemIndex];
      const quantityDifference = quantity - item.quantity;
      
      const newItems = [...state.items];
      newItems[existingItemIndex] = {
        ...item,
        quantity,
      };
      
      return {
        ...state,
        items: newItems,
        itemCount: state.itemCount + quantityDifference,
        total: state.total + (item.price * quantityDifference),
      };
    }
    
    case ActionTypes.CLEAR_CART:
      return initialState;
      
    default:
      throw new Error(`Unhandled action type: ${action.type}`);
  }
}

// Create context
const CartContext = createContext();

function CartProvider({ children }) {
  // Initialize cart from localStorage if available
  const [state, dispatch] = useReducer(cartReducer, initialState, () => {
    const savedCart = localStorage.getItem('cart');
    return savedCart ? JSON.parse(savedCart) : initialState;
  });
  
  // Save cart to localStorage when it changes
  useEffect(() => {
    localStorage.setItem('cart', JSON.stringify(state));
  }, [state]);
  
  // Helper functions for common actions
  const addItem = (item) => {
    dispatch({ type: ActionTypes.ADD_ITEM, payload: item });
  };
  
  const removeItem = (itemId) => {
    dispatch({ type: ActionTypes.REMOVE_ITEM, payload: itemId });
  };
  
  const updateQuantity = (id, quantity) => {
    dispatch({
      type: ActionTypes.UPDATE_QUANTITY,
      payload: { id, quantity },
    });
  };
  
  const clearCart = () => {
    dispatch({ type: ActionTypes.CLEAR_CART });
  };
  
  const value = {
    ...state,
    addItem,
    removeItem,
    updateQuantity,
    clearCart,
  };
  
  return (
    <CartContext.Provider value={value}>
      {children}
    </CartContext.Provider>
  );
}

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

export { CartProvider, useCart };

// Usage in components
function ProductCard({ product }) {
  const { addItem } = useCart();
  
  const handleAddToCart = () => {
    addItem(product);
  };
  
  return (
    <div className="product-card">
      <img src={product.image} alt={product.name} />
      <h3>{product.name}</h3>
      <p>${product.price.toFixed(2)}</p>
      <button onClick={handleAddToCart}>Add to Cart</button>
    </div>
  );
}

function CartIcon() {
  const { itemCount } = useCart();
  
  return (
    <div className="cart-icon">
      <span className="icon">🛒</span>
      {itemCount > 0 && <span className="badge">{itemCount}</span>}
    </div>
  );
}

function CartPage() {
  const { items, total, removeItem, updateQuantity, clearCart } = useCart();
  
  if (items.length === 0) {
    return <div>Your cart is empty</div>;
  }
  
  return (
    <div className="cart-page">
      <h1>Your Cart</h1>
      
      <table>
        <thead>
          <tr>
            <th>Product</th>
            <th>Price</th>
            <th>Quantity</th>
            <th>Total</th>
            <th>Actions</th>
          </tr>
        </thead>
        <tbody>
          {items.map(item => (
            <tr key={item.id}>
              <td>
                <div className="cart-item">
                  <img src={item.image} alt={item.name} />
                  <span>{item.name}</span>
                </div>
              </td>
              <td>${item.price.toFixed(2)}</td>
              <td>
                <input
                  type="number"
                  min="1"
                  value={item.quantity}
                  onChange={(e) => updateQuantity(item.id, parseInt(e.target.value, 10))}
                />
              </td>
              <td>${(item.price * item.quantity).toFixed(2)}</td>
              <td>
                <button onClick={() => removeItem(item.id)}>
                  Remove
                </button>
              </td>
            </tr>
          ))}
        </tbody>
        <tfoot>
          <tr>
            <td colSpan="3">Total</td>
            <td>${total.toFixed(2)}</td>
            <td>
              <button onClick={clearCart}>Clear Cart</button>
            </td>
          </tr>
        </tfoot>
      </table>
      
      <button className="checkout-button">Proceed to Checkout</button>
    </div>
  );
}
                

This shopping cart context provides a complete solution for managing cart items, quantities, and totals. It includes persistence to localStorage and helper functions for common cart operations.

Real-World Context Implementation Patterns

In production applications, you'll often see these additional patterns and best practices:

1. Context Composition

Many applications use a pattern of composing multiple contexts to create a clean separation of concerns:


// contexts/index.js
import { ThemeProvider } from './themeContext';
import { AuthProvider } from './authContext';
import { CartProvider } from './cartContext';
import { NotificationProvider } from './notificationContext';

// Create a component that composes all contexts
export function AppProviders({ children }) {
  return (
    <ThemeProvider>
      <AuthProvider>
        <NotificationProvider>
          <CartProvider>
            {children}
          </CartProvider>
        </NotificationProvider>
      </AuthProvider>
    </ThemeProvider>
  );
}
                

2. Context with API Integration

Many companies implement context that integrates directly with backend APIs:


// userContext.js with API integration
function UserProvider({ children }) {
  const [state, dispatch] = useReducer(userReducer, initialState);
  const api = useApi(); // Custom hook for API access
  
  // Function to fetch user profile
  const fetchProfile = async () => {
    dispatch({ type: 'FETCH_PROFILE_START' });
    
    try {
      const profile = await api.get('/user/profile');
      dispatch({ type: 'FETCH_PROFILE_SUCCESS', payload: profile });
      return profile;
    } catch (error) {
      dispatch({ type: 'FETCH_PROFILE_ERROR', payload: error.message });
      throw error;
    }
  };
  
  // Function to update user profile
  const updateProfile = async (data) => {
    dispatch({ type: 'UPDATE_PROFILE_START' });
    
    try {
      const updatedProfile = await api.put('/user/profile', data);
      dispatch({ type: 'UPDATE_PROFILE_SUCCESS', payload: updatedProfile });
      return updatedProfile;
    } catch (error) {
      dispatch({ type: 'UPDATE_PROFILE_ERROR', payload: error.message });
      throw error;
    }
  };
  
  // More API methods...
  
  const value = {
    ...state,
    fetchProfile,
    updateProfile,
    // More methods...
  };
  
  return (
    <UserContext.Provider value={value}>
      {children}
    </UserContext.Provider>
  );
}
                

3. Feature Flags with Context

Companies often use context to manage feature flags across the application:


// featureFlagsContext.js
function FeatureFlagsProvider({ children }) {
  const [flags, setFlags] = useState({});
  const { isAuthenticated, user } = useAuth();
  
  // Fetch feature flags on mount or when auth state changes
  useEffect(() => {
    if (isAuthenticated) {
      // Fetch user-specific feature flags
      fetchFeatureFlags(user.id).then(setFlags);
    } else {
      // Fetch public feature flags
      fetchPublicFeatureFlags().then(setFlags);
    }
  }, [isAuthenticated, user]);
  
  // Check if a feature is enabled
  const isFeatureEnabled = (featureName) => {
    return !!flags[featureName];
  };
  
  return (
    <FeatureFlagsContext.Provider value={{ flags, isFeatureEnabled }}>
      {children}
    </FeatureFlagsContext.Provider>
  );
}

// Usage in components
function NewFeature() {
  const { isFeatureEnabled } = useFeatureFlags();
  
  if (!isFeatureEnabled('new_dashboard')) {
    return null;
  }
  
  return <NewDashboard />;
}
                

Real Company Examples:

  • Airbnb: Uses context for themes, localization, and feature flags
  • Facebook: Uses context internally for UI state and user preferences
  • Shopify: Uses context for theming in Polaris, their design system
  • Atlassian: Uses context for authentication and theme in their cloud products

Many companies build internal libraries that extend Context with additional features like selector patterns, memoization, devtools integration, and more, essentially creating tailored state management solutions that fit their specific needs while leveraging React's built-in tools.

Practical Exercise: Building a Complete App Theme System

Let's apply what we've learned by building a comprehensive theme system for a React application.

Theme Context Exercise

Objective: Create a robust theme system that includes light/dark mode, custom color themes, and font size preferences.

Requirements:

  1. Create a ThemeContext with support for light/dark mode
  2. Add support for multiple color themes (default, blue, green, purple)
  3. Include font size preferences (small, medium, large)
  4. Persist theme preferences to localStorage
  5. Create a theme settings panel component
  6. Apply themes consistently across multiple components
  7. Add keyboard shortcuts for theme toggling

Implementation:

1. Theme Context Setup


  // src/contexts/ThemeContext.js
  import React, { createContext, useContext, useReducer, useEffect } from 'react';
  
  // Theme options
  const MODES = {
    LIGHT: 'light',
    DARK: 'dark',
  };
  
  const COLOR_THEMES = {
    DEFAULT: 'default',
    BLUE: 'blue',
    GREEN: 'green',
    PURPLE: 'purple',
  };
  
  const FONT_SIZES = {
    SMALL: 'small',
    MEDIUM: 'medium',
    LARGE: 'large',
  };
  
  // Initial state
  const initialState = {
    mode: MODES.LIGHT,
    colorTheme: COLOR_THEMES.DEFAULT,
    fontSize: FONT_SIZES.MEDIUM,
  };
  
  // Action types
  const ActionTypes = {
    SET_MODE: 'theme/setMode',
    SET_COLOR_THEME: 'theme/setColorTheme',
    SET_FONT_SIZE: 'theme/setFontSize',
    RESET_THEME: 'theme/resetTheme',
  };
  
  // Reducer function
  function themeReducer(state, action) {
    switch (action.type) {
      case ActionTypes.SET_MODE:
        return {
          ...state,
          mode: action.payload,
        };
      case ActionTypes.SET_COLOR_THEME:
        return {
          ...state,
          colorTheme: action.payload,
        };
      case ActionTypes.SET_FONT_SIZE:
        return {
          ...state,
          fontSize: action.payload,
        };
      case ActionTypes.RESET_THEME:
        return initialState;
      default:
        throw new Error(`Unhandled action type: ${action.type}`);
    }
  }
  
  // Create context
  const ThemeContext = createContext();
  
  // Provider component
  function ThemeProvider({ children }) {
    // Initialize state from localStorage or defaults
    const [state, dispatch] = useReducer(themeReducer, initialState, () => {
      const savedTheme = localStorage.getItem('theme');
      return savedTheme ? JSON.parse(savedTheme) : initialState;
    });
    
    // Save to localStorage when theme changes
    useEffect(() => {
      localStorage.setItem('theme', JSON.stringify(state));
    }, [state]);
    
    // Apply theme to document
    useEffect(() => {
      // Apply mode (light/dark)
      document.documentElement.setAttribute('data-theme-mode', state.mode);
      
      // Apply color theme
      document.documentElement.setAttribute('data-theme-color', state.colorTheme);
      
      // Apply font size
      document.documentElement.setAttribute('data-theme-font-size', state.fontSize);
      
      // Set body class for easier styling
      document.body.className = `theme-${state.mode} color-${state.colorTheme} font-${state.fontSize}`;
    }, [state.mode, state.colorTheme, state.fontSize]);
    
    // Keyboard shortcuts
    useEffect(() => {
      const handleKeyDown = (e) => {
        // Shift + D to toggle between light and dark mode
        if (e.shiftKey && e.key === 'D') {
          toggleMode();
        }
        
        // Shift + C to cycle through color themes
        if (e.shiftKey && e.key === 'C') {
          cycleColorTheme();
        }
        
        // Shift + F to cycle through font sizes
        if (e.shiftKey && e.key === 'F') {
          cycleFontSize();
        }
      };
      
      window.addEventListener('keydown', handleKeyDown);
      
      return () => {
        window.removeEventListener('keydown', handleKeyDown);
      };
    }, [state]); // We need the current state to toggle correctly
    
    // Helper function to get CSS variables for current theme
    const getThemeVariables = () => {
      const variables = {
        // Base colors
        light: {
          default: {
            '--bg-primary': '#ffffff',
            '--bg-secondary': '#f8f9fa',
            '--text-primary': '#212529',
            '--text-secondary': '#6c757d',
            '--accent-color': '#007bff',
          },
          blue: {
            '--bg-primary': '#f0f8ff',
            '--bg-secondary': '#e6f2ff',
            '--text-primary': '#0a2744',
            '--text-secondary': '#4a6d8c',
            '--accent-color': '#0066cc',
          },
          green: {
            '--bg-primary': '#f0fff4',
            '--bg-secondary': '#e6ffee',
            '--text-primary': '#0a4428',
            '--text-secondary': '#3d7a5d',
            '--accent-color': '#00a650',
          },
          purple: {
            '--bg-primary': '#f8f0ff',
            '--bg-secondary': '#f2e6ff',
            '--text-primary': '#4a0a77',
            '--text-secondary': '#7a4a9c',
            '--accent-color': '#6200ee',
          },
        },
        dark: {
          default: {
            '--bg-primary': '#212529',
            '--bg-secondary': '#343a40',
            '--text-primary': '#f8f9fa',
            '--text-secondary': '#ced4da',
            '--accent-color': '#0d6efd',
          },
          blue: {
            '--bg-primary': '#0a1929',
            '--bg-secondary': '#162a41',
            '--text-primary': '#e6f2ff',
            '--text-secondary': '#a3c6e8',
            '--accent-color': '#4da6ff',
          },
          green: {
            '--bg-primary': '#0a291e',
            '--bg-secondary': '#193d30',
            '--text-primary': '#e6ffe6',
            '--text-secondary': '#a3e8a3',
            '--accent-color': '#66cc99',
          },
          purple: {
            '--bg-primary': '#1a0933',
            '--bg-secondary': '#2d1554',
            '--text-primary': '#f2e6ff',
            '--text-secondary': '#d0b3ff',
            '--accent-color': '#9966ff',
          },
        },
      };
      
      // Font size variables
      const fontSizes = {
        small: {
          '--font-size-base': '0.875rem',
          '--font-size-lg': '1rem',
          '--font-size-xl': '1.125rem',
          '--font-size-2xl': '1.25rem',
          '--font-size-3xl': '1.5rem',
        },
        medium: {
          '--font-size-base': '1rem',
          '--font-size-lg': '1.125rem',
          '--font-size-xl': '1.25rem',
          '--font-size-2xl': '1.5rem',
          '--font-size-3xl': '1.875rem',
        },
        large: {
          '--font-size-base': '1.125rem',
          '--font-size-lg': '1.25rem',
          '--font-size-xl': '1.5rem',
          '--font-size-2xl': '1.875rem',
          '--font-size-3xl': '2.25rem',
        },
      };
      
      // Combine variables based on current settings
      return {
        ...variables[state.mode][state.colorTheme],
        ...fontSizes[state.fontSize],
      };
    };
    
    // Action creators
    const setMode = (mode) => {
      dispatch({ type: ActionTypes.SET_MODE, payload: mode });
    };
    
    const toggleMode = () => {
      const newMode = state.mode === MODES.LIGHT ? MODES.DARK : MODES.LIGHT;
      setMode(newMode);
    };
    
    const setColorTheme = (colorTheme) => {
      dispatch({ type: ActionTypes.SET_COLOR_THEME, payload: colorTheme });
    };
    
    const cycleColorTheme = () => {
      const themes = Object.values(COLOR_THEMES);
      const currentIndex = themes.indexOf(state.colorTheme);
      const nextIndex = (currentIndex + 1) % themes.length;
      setColorTheme(themes[nextIndex]);
    };
    
    const setFontSize = (fontSize) => {
      dispatch({ type: ActionTypes.SET_FONT_SIZE, payload: fontSize });
    };
    
    const cycleFontSize = () => {
      const sizes = Object.values(FONT_SIZES);
      const currentIndex = sizes.indexOf(state.fontSize);
      const nextIndex = (currentIndex + 1) % sizes.length;
      setFontSize(sizes[nextIndex]);
    };
    
    const resetTheme = () => {
      dispatch({ type: ActionTypes.RESET_THEME });
    };
    
    // Context value
    const value = {
      ...state,
      MODES,
      COLOR_THEMES,
      FONT_SIZES,
      setMode,
      toggleMode,
      setColorTheme,
      cycleColorTheme,
      setFontSize,
      cycleFontSize,
      resetTheme,
      getThemeVariables,
    };
    
    return (
      <ThemeContext.Provider value={value}>
        {children}
      </ThemeContext.Provider>
    );
  }
  
  // Custom hook for consuming the context
  function useTheme() {
    const context = useContext(ThemeContext);
    
    if (context === undefined) {
      throw new Error('useTheme must be used within a ThemeProvider');
    }
    
    return context;
  }
  
  export { ThemeProvider, useTheme, MODES, COLOR_THEMES, FONT_SIZES };
                  

2. Theme Settings Panel Component


  // src/components/ThemeSettings.js
  import React from 'react';
  import { useTheme, MODES, COLOR_THEMES, FONT_SIZES } from '../contexts/ThemeContext';
  
  function ThemeSettings() {
    const { 
      mode, 
      colorTheme, 
      fontSize, 
      setMode, 
      setColorTheme, 
      setFontSize, 
      resetTheme 
    } = useTheme();
    
    return (
      <div className="theme-settings">
        <h2>Theme Settings</h2>
        
        <div className="setting-group">
          <h3>Mode</h3>
          <div className="setting-controls">
            {Object.values(MODES).map((themeMode) => (
              <button
                key={themeMode}
                className={`mode-button ${mode === themeMode ? 'active' : ''}`}
                onClick={() => setMode(themeMode)}
              >
                {themeMode.charAt(0).toUpperCase() + themeMode.slice(1)}
              </button>
            ))}
          </div>
        </div>
        
        <div className="setting-group">
          <h3>Color Theme</h3>
          <div className="setting-controls">
            {Object.values(COLOR_THEMES).map((theme) => (
              <button
                key={theme}
                className={`color-button ${colorTheme === theme ? 'active' : ''} ${theme}`}
                onClick={() => setColorTheme(theme)}
                title={theme.charAt(0).toUpperCase() + theme.slice(1)}
              >
                <span className="color-preview"></span>
              </button>
            ))}
          </div>
        </div>
        
        <div className="setting-group">
          <h3>Font Size</h3>
          <div className="setting-controls">
            {Object.values(FONT_SIZES).map((size) => (
              <button
                key={size}
                className={`size-button ${fontSize === size ? 'active' : ''}`}
                onClick={() => setFontSize(size)}
              >
                {size.charAt(0).toUpperCase() + size.slice(1)}
              </button>
            ))}
          </div>
        </div>
        
        <div className="setting-group">
          <button className="reset-button" onClick={resetTheme}>
            Reset to Defaults
          </button>
        </div>
        
        <div className="keyboard-shortcuts">
          <h3>Keyboard Shortcuts</h3>
          <ul>
            <li><kbd>Shift</kbd> + <kbd>D</kbd>: Toggle dark/light mode</li>
            <li><kbd>Shift</kbd> + <kbd>C</kbd>: Cycle color themes</li>
            <li><kbd>Shift</kbd> + <kbd>F</kbd>: Cycle font sizes</li>
          </ul>
        </div>
      </div>
    );
  }
  
  export default ThemeSettings;
                  

3. Theme Styling Component


  // src/components/ThemeProvider.js
  import React, { useEffect } from 'react';
  import { useTheme } from '../contexts/ThemeContext';
  
  // This component applies theme variables to the document
  function ThemeStyleProvider({ children }) {
    const { getThemeVariables } = useTheme();
    
    useEffect(() => {
      // Get the current theme variables
      const variables = getThemeVariables();
      
      // Apply variables to document root
      Object.entries(variables).forEach(([property, value]) => {
        document.documentElement.style.setProperty(property, value);
      });
    }, [getThemeVariables]);
    
    return <>{children};
  }
  
  export default ThemeStyleProvider;
                  

4. App Setup with Theme Context


  // src/App.js
  import React, { useState } from 'react';
  import { ThemeProvider } from './contexts/ThemeContext';
  import ThemeStyleProvider from './components/ThemeStyleProvider';
  import ThemeSettings from './components/ThemeSettings';
  import './App.css';
  
  function App() {
    const [showSettings, setShowSettings] = useState(false);
    
    return (
      <ThemeProvider>
        <ThemeStyleProvider>
          <div className="app">
            <header className="app-header">
              <h1>Theme Context Demo</h1>
              <button 
                className="settings-toggle"
                onClick={() => setShowSettings(!showSettings)}
              >
                {showSettings ? 'Hide Settings' : 'Show Settings'}
              </button>
            </header>
            
            {showSettings && (
              <div className="settings-panel">
                <ThemeSettings />
              </div>
            )}
            
            <main className="app-content">
              <section className="demo-section">
                <h2>Content Section</h2>
                <p>
                  This text will adapt to your chosen theme. Try changing the theme
                  settings to see how the colors and font size update.
                </p>
                
                <div className="card">
                  <h3>Card Title</h3>
                  <p>
                    This is a card component that demonstrates how different elements
                    respond to theme changes.
                  </p>
                  <button className="primary-button">Primary Button</button>
                  <button className="secondary-button">Secondary Button</button>
                </div>
                
                <div className="card">
                  <h3>Typography Example</h3>
                  <h1>Heading 1</h1>
                  <h2>Heading 2</h2>
                  <h3>Heading 3</h3>
                  <h4>Heading 4</h4>
                  <p>Regular paragraph text</p>
                  <p><small>Small text</small></p>
                </div>
              </section>
            </main>
            
            <footer className="app-footer">
              <p>Try using the keyboard shortcuts to change the theme!</p>
            </footer>
          </div>
        </ThemeStyleProvider>
      </ThemeProvider>
    );
  }
  
  export default App;
                  

5. CSS for Theme Support


  /* src/App.css */
  /* Base styles with CSS variables */
  :root {
    /* Default variables, will be overridden by ThemeStyleProvider */
    --bg-primary: #ffffff;
    --bg-secondary: #f8f9fa;
    --text-primary: #212529;
    --text-secondary: #6c757d;
    --accent-color: #007bff;
    
    --font-size-base: 1rem;
    --font-size-lg: 1.125rem;
    --font-size-xl: 1.25rem;
    --font-size-2xl: 1.5rem;
    --font-size-3xl: 1.875rem;
    
    --transition-speed: 0.3s;
  }
  
  /* Global styles */
  * {
    box-sizing: border-box;
    transition: background-color var(--transition-speed), color var(--transition-speed);
  }
  
  body {
    margin: 0;
    font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen,
      Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
    font-size: var(--font-size-base);
    line-height: 1.5;
    background-color: var(--bg-primary);
    color: var(--text-primary);
  }
  
  .app {
    min-height: 100vh;
    display: flex;
    flex-direction: column;
  }
  
  /* Header styles */
  .app-header {
    background-color: var(--accent-color);
    color: white;
    padding: 1rem;
    display: flex;
    justify-content: space-between;
    align-items: center;
  }
  
  .app-header h1 {
    margin: 0;
    font-size: var(--font-size-2xl);
  }
  
  .settings-toggle {
    background-color: transparent;
    border: 2px solid white;
    color: white;
    padding: 0.5rem 1rem;
    border-radius: 4px;
    cursor: pointer;
    font-size: var(--font-size-base);
  }
  
  .settings-toggle:hover {
    background-color: rgba(255, 255, 255, 0.1);
  }
  
  /* Settings panel */
  .settings-panel {
    background-color: var(--bg-secondary);
    padding: 1rem;
    border-bottom: 1px solid rgba(0, 0, 0, 0.1);
  }
  
  .theme-settings {
    max-width: 600px;
    margin: 0 auto;
  }
  
  .theme-settings h2 {
    font-size: var(--font-size-xl);
    margin-top: 0;
    margin-bottom: 1rem;
  }
  
  .setting-group {
    margin-bottom: 1.5rem;
  }
  
  .setting-group h3 {
    font-size: var(--font-size-lg);
    margin-bottom: 0.5rem;
    color: var(--text-primary);
  }
  
  .setting-controls {
    display: flex;
    gap: 0.5rem;
    flex-wrap: wrap;
  }
  
  /* Setting buttons */
  .setting-controls button {
    padding: 0.5rem 1rem;
    border: 1px solid var(--text-secondary);
    background-color: var(--bg-primary);
    color: var(--text-primary);
    border-radius: 4px;
    cursor: pointer;
    font-size: var(--font-size-base);
  }
  
  .setting-controls button.active {
    background-color: var(--accent-color);
    color: white;
    border-color: var(--accent-color);
  }
  
  .color-button {
    width: 40px;
    height: 40px;
    padding: 0 !important;
    display: flex;
    align-items: center;
    justify-content: center;
  }
  
  .color-preview {
    width: 24px;
    height: 24px;
    border-radius: 50%;
  }
  
  .color-button.default .color-preview {
    background-color: #007bff;
  }
  
  .color-button.blue .color-preview {
    background-color: #0066cc;
  }
  
  .color-button.green .color-preview {
    background-color: #00a650;
  }
  
  .color-button.purple .color-preview {
    background-color: #6200ee;
  }
  
  .reset-button {
    background-color: var(--text-secondary);
    color: white;
    border: none;
    padding: 0.5rem 1rem;
    border-radius: 4px;
    cursor: pointer;
    font-size: var(--font-size-base);
  }
  
  .reset-button:hover {
    opacity: 0.9;
  }
  
  /* Keyboard shortcuts */
  .keyboard-shortcuts {
    background-color: rgba(0, 0, 0, 0.05);
    padding: 0.75rem;
    border-radius: 4px;
    margin-top: 1rem;
  }
  
  .keyboard-shortcuts h3 {
    font-size: var(--font-size-base);
    margin-top: 0;
    margin-bottom: 0.5rem;
  }
  
  .keyboard-shortcuts ul {
    list-style-type: none;
    padding-left: 0;
    margin: 0;
  }
  
  .keyboard-shortcuts li {
    margin-bottom: 0.25rem;
  }
  
  kbd {
    background-color: var(--bg-primary);
    border: 1px solid var(--text-secondary);
    border-radius: 3px;
    box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2);
    padding: 0.1rem 0.4rem;
    font-size: 0.9em;
  }
  
  /* Main content */
  .app-content {
    flex: 1;
    padding: 1rem;
    max-width: 800px;
    margin: 0 auto;
    width: 100%;
  }
  
  .demo-section h2 {
    font-size: var(--font-size-2xl);
    margin-bottom: 1rem;
  }
  
  .card {
    background-color: var(--bg-secondary);
    border-radius: 8px;
    padding: 1.5rem;
    margin-bottom: 1.5rem;
    box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
  }
  
  .card h3 {
    font-size: var(--font-size-xl);
    margin-top: 0;
    margin-bottom: 1rem;
  }
  
  /* Typography */
  h1 {
    font-size: var(--font-size-3xl);
  }
  
  h2 {
    font-size: var(--font-size-2xl);
  }
  
  h3 {
    font-size: var(--font-size-xl);
  }
  
  h4 {
    font-size: var(--font-size-lg);
  }
  
  small {
    font-size: 0.875em;
    color: var(--text-secondary);
  }
  
  /* Buttons */
  .primary-button, 
  .secondary-button {
    padding: 0.5rem 1rem;
    border-radius: 4px;
    font-size: var(--font-size-base);
    cursor: pointer;
    margin-right: 0.5rem;
  }
  
  .primary-button {
    background-color: var(--accent-color);
    color: white;
    border: none;
  }
  
  .secondary-button {
    background-color: transparent;
    color: var(--accent-color);
    border: 1px solid var(--accent-color);
  }
  
  /* Footer */
  .app-footer {
    background-color: var(--bg-secondary);
    padding: 1rem;
    text-align: center;
    color: var(--text-secondary);
  }
                  

Bonus Challenge: Extend this theme system with:

  • Additional theme properties like border radius or spacing scale
  • Theme export/import functionality
  • System preference detection (to match OS dark/light mode)
  • Custom theme creation UI

Further Resources