React Context API - Complete Guide

React Context API - Complete Guide

React Context API provides a way to pass data through the component tree without having to pass props down manually at every level. It’s designed for sharing “global” data like theme, user authentication, or language settings.

What is React Context?

Context is a way to share values between components without explicitly passing props through every level of the component tree. It solves the problem of “prop drilling” - passing props through intermediate components that don’t need them.

When to Use Context

Use Context when:

  • You need to share data between many components at different nesting levels
  • You have global data like user info, theme, or language
  • You want to avoid prop drilling

Don’t use Context just because it’s easier than passing props. Use it when the data is truly global.

Basic Context Usage

1. Creating Context

import { createContext } from 'react';

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

export default ThemeContext;

2. Providing Context

import { 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 />
        <Main />
        <Footer />
      </div>
    </ThemeContext.Provider>
  );
}

3. Consuming Context

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

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

Complete Context Example

User Authentication Context

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

const AuthContext = createContext(null);

export function AuthProvider({ children }) {
  const [user, setUser] = useState(null);
  const [isLoading, setIsLoading] = useState(false);
  
  const login = async (email, password) => {
    setIsLoading(true);
    try {
      // Simulate API call
      await new Promise(resolve => setTimeout(resolve, 1000));
      
      // Mock user data
      const userData = {
        id: 1,
        name: 'John Doe',
        email: email,
        token: 'mock-jwt-token'
      };
      
      setUser(userData);
      localStorage.setItem('user', JSON.stringify(userData));
    } catch (error) {
      console.error('Login failed:', error);
    } finally {
      setIsLoading(false);
    }
  };
  
  const logout = () => {
    setUser(null);
    localStorage.removeItem('user');
  };
  
  const register = async (name, email, password) => {
    setIsLoading(true);
    try {
      // Simulate API call
      await new Promise(resolve => setTimeout(resolve, 1000));
      
      const userData = {
        id: Date.now(),
        name: name,
        email: email,
        token: 'mock-jwt-token'
      };
      
      setUser(userData);
      localStorage.setItem('user', JSON.stringify(userData));
    } catch (error) {
      console.error('Registration failed:', error);
    } finally {
      setIsLoading(false);
    }
  };
  
  // Check for existing user on mount
  useState(() => {
    const savedUser = localStorage.getItem('user');
    if (savedUser) {
      setUser(JSON.parse(savedUser));
    }
  });
  
  const value = {
    user,
    isLoading,
    login,
    logout,
    register,
    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 an AuthProvider');
  }
  return context;
}

Using the Auth Context

// Login.js
import { useState } from 'react';
import { useAuth } from './AuthContext';

function Login() {
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');
  const { login, isLoading } = useAuth();
  
  const handleSubmit = async (e) => {
    e.preventDefault();
    await login(email, password);
  };
  
  return (
    <form onSubmit={handleSubmit}>
      <h2>Login</h2>
      
      <div>
        <label>Email:</label>
        <input
          type="email"
          value={email}
          onChange={(e) => setEmail(e.target.value)}
          required
        />
      </div>
      
      <div>
        <label>Password:</label>
        <input
          type="password"
          value={password}
          onChange={(e) => setPassword(e.target.value)}
          required
        />
      </div>
      
      <button type="submit" disabled={isLoading}>
        {isLoading ? 'Logging in...' : 'Login'}
      </button>
    </form>
  );
}

// ProtectedRoute.js
import { useAuth } from './AuthContext';

function ProtectedRoute({ children }) {
  const { isAuthenticated, user } = useAuth();
  
  if (!isAuthenticated) {
    return <div>Please log in to access this page.</div>;
  }
  
  return <>{children}</>;
}

// Dashboard.js
import { useAuth } from './AuthContext';

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

// App.js
import { AuthProvider } from './AuthContext';

function App() {
  return (
    <AuthProvider>
      <Router>
        <Routes>
          <Route path="/login" element={<Login />} />
          <Route path="/dashboard" element={
            <ProtectedRoute>
              <Dashboard />
            </ProtectedRoute>
          } />
        </Routes>
      </Router>
    </AuthProvider>
  );
}

Theme Context Example

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

const ThemeContext = createContext(null);

export function ThemeProvider({ children }) {
  const [theme, setTheme] = useState('light');
  
  useEffect(() => {
    // Load theme from localStorage
    const savedTheme = localStorage.getItem('theme');
    if (savedTheme) {
      setTheme(savedTheme);
    }
  }, []);
  
  useEffect(() => {
    // Apply theme to document
    document.documentElement.setAttribute('data-theme', theme);
    localStorage.setItem('theme', theme);
  }, [theme]);
  
  const toggleTheme = () => {
    setTheme(prevTheme => prevTheme === 'light' ? 'dark' : 'light');
  };
  
  const setThemeMode = (mode) => {
    setTheme(mode);
  };
  
  const value = {
    theme,
    toggleTheme,
    setThemeMode,
    isLight: theme === 'light',
    isDark: theme === 'dark'
  };
  
  return (
    <ThemeContext.Provider value={value}>
      {children}
    </ThemeContext.Provider>
  );
}

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

Using Theme Context

// ThemeButton.js
import { useTheme } from './ThemeContext';

function ThemeButton() {
  const { theme, toggleTheme } = useTheme();
  
  return (
    <button onClick={toggleTheme} className="theme-toggle">
      {theme === 'light' ? '🌙' : '☀️'} {theme === 'light' ? 'Dark' : 'Light'} Mode
    </button>
  );
}

// ThemedComponent.js
import { useTheme } from './ThemeContext';

function ThemedComponent() {
  const { theme, isLight, isDark } = useTheme();
  
  return (
    <div className={`themed-component ${theme}`}>
      <h2>Current Theme: {theme}</h2>
      <p>This component adapts to the theme.</p>
      
      {isLight && <p>Light mode is active</p>}
      {isDark && <p>Dark mode is active</p>}
      
      <style jsx>{`
        .themed-component {
          padding: 20px;
          border-radius: 8px;
          transition: all 0.3s ease;
        }
        
        .themed-component.light {
          background-color: #ffffff;
          color: #333333;
          border: 1px solid #e0e0e0;
        }
        
        .themed-component.dark {
          background-color: #1a1a1a;
          color: #ffffff;
          border: 1px solid #333333;
        }
      `}</style>
    </div>
  );
}

Shopping Cart Context Example

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

const CartContext = createContext(null);

export function CartProvider({ children }) {
  const [items, setItems] = useState([]);
  
  const addToCart = (product) => {
    setItems(prevItems => {
      const existingItem = prevItems.find(item => item.id === product.id);
      
      if (existingItem) {
        return prevItems.map(item =>
          item.id === product.id
            ? { ...item, quantity: item.quantity + 1 }
            : item
        );
      }
      
      return [...prevItems, { ...product, quantity: 1 }];
    });
  };
  
  const removeFromCart = (productId) => {
    setItems(prevItems => prevItems.filter(item => item.id !== productId));
  };
  
  const updateQuantity = (productId, quantity) => {
    if (quantity <= 0) {
      removeFromCart(productId);
      return;
    }
    
    setItems(prevItems =>
      prevItems.map(item =>
        item.id === productId
          ? { ...item, quantity }
          : item
      )
    );
  };
  
  const clearCart = () => {
    setItems([]);
  };
  
  const getTotalPrice = () => {
    return items.reduce((total, item) => total + (item.price * item.quantity), 0);
  };
  
  const getTotalItems = () => {
    return items.reduce((total, item) => total + item.quantity, 0);
  };
  
  const value = {
    items,
    addToCart,
    removeFromCart,
    updateQuantity,
    clearCart,
    getTotalPrice,
    getTotalItems,
    isEmpty: items.length === 0
  };
  
  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 a CartProvider');
  }
  return context;
}

Using Cart Context

// Product.js
import { useCart } from './CartContext';

function Product({ product }) {
  const { addToCart } = useCart();
  
  return (
    <div className="product">
      <h3>{product.name}</h3>
      <p>${product.price}</p>
      <button onClick={() => addToCart(product)}>
        Add to Cart
      </button>
    </div>
  );
}

// Cart.js
import { useCart } from './CartContext';

function Cart() {
  const { items, removeFromCart, updateQuantity, getTotalPrice, isEmpty } = useCart();
  
  if (isEmpty) {
    return <div>Your cart is empty</div>;
  }
  
  return (
    <div className="cart">
      <h2>Shopping Cart</h2>
      
      {items.map(item => (
        <div key={item.id} className="cart-item">
          <span>{item.name}</span>
          <span>${item.price}</span>
          
          <div className="quantity-controls">
            <button 
              onClick={() => updateQuantity(item.id, item.quantity - 1)}
            >
              -
            </button>
            <span>{item.quantity}</span>
            <button 
              onClick={() => updateQuantity(item.id, item.quantity + 1)}
            >
              +
            </button>
          </div>
          
          <button onClick={() => removeFromCart(item.id)}>
            Remove
          </button>
        </div>
      ))}
      
      <div className="cart-total">
        <strong>Total: ${getTotalPrice().toFixed(2)}</strong>
      </div>
    </div>
  );
}

// CartIcon.js
import { useCart } from './CartContext';

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

Advanced Context Patterns

1. Context with Reducer

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

const initialState = {
  user: null,
  theme: 'light',
  notifications: []
};

function appReducer(state, action) {
  switch (action.type) {
    case 'SET_USER':
      return { ...state, user: action.payload };
    case 'SET_THEME':
      return { ...state, theme: action.payload };
    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 'LOGOUT':
      return { ...state, user: null };
    default:
      return state;
  }
}

const AppContext = createContext(null);

export function AppProvider({ children }) {
  const [state, dispatch] = useReducer(appReducer, initialState);
  
  const value = {
    ...state,
    dispatch,
    setUser: (user) => dispatch({ type: 'SET_USER', payload: user }),
    setTheme: (theme) => dispatch({ type: 'SET_THEME', payload: theme }),
    addNotification: (notification) => dispatch({
      type: 'ADD_NOTIFICATION',
      payload: { ...notification, id: Date.now() }
    }),
    removeNotification: (id) => dispatch({
      type: 'REMOVE_NOTIFICATION',
      payload: id
    }),
    logout: () => dispatch({ type: 'LOGOUT' })
  };
  
  return (
    <AppContext.Provider value={value}>
      {children}
    </AppContext.Provider>
  );
}

export function useApp() {
  const context = useContext(AppContext);
  if (!context) {
    throw new Error('useApp must be used within an AppProvider');
  }
  return context;
}

2. Multiple Contexts

// App.js
import { AuthProvider } from './AuthContext';
import { ThemeProvider } from './ThemeContext';
import { CartProvider } from './CartContext';

function App() {
  return (
    <AuthProvider>
      <ThemeProvider>
        <CartProvider>
          <Router>
            <Routes>
              {/* Your routes here */}
            </Routes>
          </Router>
        </CartProvider>
      </ThemeProvider>
    </AuthProvider>
  );
}

3. Context Selectors

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

const UserContext = createContext(null);

export function UserProvider({ children }) {
  const [user, setUser] = useState(null);
  const [preferences, setPreferences] = useState({});
  
  const value = {
    user,
    setUser,
    preferences,
    setPreferences
  };
  
  return (
    <UserContext.Provider value={value}>
      {children}
    </UserContext.Provider>
  );
}

// Custom hooks for specific context values
export function useUser() {
  const { user, setUser } = useContext(UserContext);
  return { user, setUser };
}

export function useUserPreferences() {
  const { preferences, setPreferences } = useContext(UserContext);
  return { preferences, setPreferences };
}

Performance Considerations

1. Memoizing Context Value

import { useMemo } from 'react';

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

2. Splitting Context

// Instead of one large context, split into smaller ones
const UserContext = createContext(null);
const ThemeContext = createContext(null);
const NotificationContext = createContext(null);

// Components only subscribe to the contexts they need
function UserProfile() {
  const { user } = useContext(UserContext); // Only re-renders when user changes
  return <div>{user.name}</div>;
}

function ThemeToggle() {
  const { theme, toggleTheme } = useContext(ThemeContext); // Only re-renders when theme changes
  return <button onClick={toggleTheme}>Toggle Theme</button>;
}

Best Practices

1. Create Custom Hooks

// Good - custom hook for context
export function useAuth() {
  const context = useContext(AuthContext);
  if (!context) {
    throw new Error('useAuth must be used within an AuthProvider');
  }
  return context;
}

// Usage
function MyComponent() {
  const { user, login } = useAuth(); // Clean and type-safe
}

2. Provide Default Values

// Good - provide meaningful default values
const ThemeContext = createContext({
  theme: 'light',
  toggleTheme: () => {}
});

3. Don’t Overuse Context

// Bad - using context for component-specific state
const ButtonContext = createContext(null);

// Good - using props for component-specific state
function Button({ onClick, children }) {
  return <button onClick={onClick}>{children}</button>;
}

Summary

React Context API is powerful for sharing global data:

  • Use Context for global state like auth, theme, language
  • Create custom hooks for clean context consumption
  • Provide meaningful default values
  • Split large contexts into smaller ones for better performance
  • Memoize context values to prevent unnecessary re-renders
  • Don’t use Context for component-specific state
  • Always handle the case when context is undefined

Mastering Context API will help you build scalable React applications with clean state management and reduced prop drilling.


External Resources:

Related Tutorials:

Last updated on