React Hooks Deep Dive: useContext

Managing Global State Like a Pro

Welcome to Week 5: Advanced React Concepts!

Today we're diving into one of React's most powerful hooks: useContext. Imagine you're running a pizza restaurant, and you need to share the customer's order details across multiple stations: the order counter, kitchen, and delivery desk. That's exactly what useContext does for our React applications - it shares data across different components without passing props through every level.

The Problem: Prop Drilling

Before we understand the solution, let's look at the problem. Imagine building a theme switcher app:

// Without useContext - Prop Drilling Hell! 😱
function App() {
  const [theme, setTheme] = useState('light');
  
  return (
    <div>
      <Navbar theme={theme} />
      <MainContent theme={theme} />
      <Footer theme={theme} />
    </div>
  );
}

function Navbar({ theme }) {
  return (
    <nav>
      <Logo theme={theme} />
      <NavLinks theme={theme} />
    </nav>
  );
}

function NavLinks({ theme }) {
  return (
    <ul>
      <NavLink theme={theme} text="Home" />
      <NavLink theme={theme} text="About" />
    </ul>
  );
}

// And it keeps going deeper... 😵
graph TD A[App - theme state] --> B[Navbar] A --> C[MainContent] A --> D[Footer] B --> E[Logo] B --> F[NavLinks] F --> G[NavLink] F --> H[NavLink] style A fill:#ff9999,stroke:#333,stroke-width:4px style G fill:#99ff99,stroke:#333,stroke-width:2px style H fill:#99ff99,stroke:#333,stroke-width:2px

The Solution: Context API + useContext

Context is like a global warehouse for your data. Any component can access it directly without the data passing through every component in between.

// Step 1: Create the Context
const ThemeContext = React.createContext();

// Step 2: Create a Provider Component
function ThemeProvider({ children }) {
  const [theme, setTheme] = useState('light');
  
  const toggleTheme = () => {
    setTheme(prevTheme => prevTheme === 'light' ? 'dark' : 'light');
  };
  
  return (
    <ThemeContext.Provider value={{ theme, toggleTheme }}>
      {children}
    </ThemeContext.Provider>
  );
}

// Step 3: Use the Context
function NavLink({ text }) {
  const { theme } = useContext(ThemeContext);
  
  return (
    <li className={`nav-link ${theme}`}>
      {text}
    </li>
  );
}

// Step 4: Wrap your app
function App() {
  return (
    <ThemeProvider>
      <Navbar />
      <MainContent />
      <Footer />
    </ThemeProvider>
  );
}
graph TD A[ThemeProvider] --> B[ThemeContext] B --> C[App] C --> D[Navbar] C --> E[MainContent] C --> F[Footer] B -.-> G[NavLink] B -.-> H[Button] B -.-> I[Any Component] style A fill:#ff9999,stroke:#333,stroke-width:4px style B fill:#ffcc99,stroke:#333,stroke-width:4px style G fill:#99ff99,stroke:#333,stroke-width:2px style H fill:#99ff99,stroke:#333,stroke-width:2px style I fill:#99ff99,stroke:#333,stroke-width:2px

Real-World Example: Shopping Cart System

Let's build a mini e-commerce application with a global shopping cart:

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

const CartContext = createContext();

const cartReducer = (state, action) => {
  switch (action.type) {
    case 'ADD_ITEM':
      const existingItem = state.items.find(item => item.id === action.payload.id);
      if (existingItem) {
        return {
          ...state,
          items: state.items.map(item =>
            item.id === action.payload.id
              ? { ...item, quantity: item.quantity + 1 }
              : item
          )
        };
      }
      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
        )
      };
      
    default:
      return state;
  }
};

export function CartProvider({ children }) {
  const [state, dispatch] = useReducer(cartReducer, { items: [] });
  
  const addToCart = (product) => {
    dispatch({ type: 'ADD_ITEM', payload: product });
  };
  
  const removeFromCart = (productId) => {
    dispatch({ type: 'REMOVE_ITEM', payload: productId });
  };
  
  const updateQuantity = (productId, quantity) => {
    dispatch({ type: 'UPDATE_QUANTITY', payload: { id: productId, quantity } });
  };
  
  const cartTotal = state.items.reduce(
    (total, item) => total + item.price * item.quantity,
    0
  );
  
  return (
    <CartContext.Provider 
      value={{ 
        items: state.items, 
        addToCart, 
        removeFromCart, 
        updateQuantity, 
        cartTotal 
      }}
    >
      {children}
    </CartContext.Provider>
  );
}

// Custom hook for easier usage
export function useCart() {
  const context = useContext(CartContext);
  if (!context) {
    throw new Error('useCart must be used within a CartProvider');
  }
  return context;
}
// Product.js
function Product({ product }) {
  const { addToCart } = useCart();
  
  return (
    <div className="product-card">
      <img src={product.image} alt={product.name} />
      <h3>{product.name}</h3>
      <p>${product.price}</p>
      <button onClick={() => addToCart(product)}>
        Add to Cart
      </button>
    </div>
  );
}

// CartIcon.js
function CartIcon() {
  const { items } = useCart();
  const itemCount = items.reduce((sum, item) => sum + item.quantity, 0);
  
  return (
    <div className="cart-icon">
      🛒 {itemCount > 0 && <span className="badge">{itemCount}</span>}
    </div>
  );
}

Common Context Patterns

Pattern 1: Authentication Context

const AuthContext = createContext();

function AuthProvider({ children }) {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);
  
  useEffect(() => {
    // Check if user is logged in
    const token = localStorage.getItem('token');
    if (token) {
      fetchUser(token).then(userData => {
        setUser(userData);
        setLoading(false);
      });
    } else {
      setLoading(false);
    }
  }, []);
  
  const login = async (email, password) => {
    const response = await authAPI.login(email, password);
    setUser(response.user);
    localStorage.setItem('token', response.token);
  };
  
  const logout = () => {
    setUser(null);
    localStorage.removeItem('token');
  };
  
  if (loading) {
    return <LoadingSpinner />;
  }
  
  return (
    <AuthContext.Provider value={{ user, login, logout }}>
      {children}
    </AuthContext.Provider>
  );
}

Pattern 2: Multi-Language Support

const LanguageContext = createContext();

const translations = {
  en: {
    welcome: 'Welcome',
    goodbye: 'Goodbye',
    addToCart: 'Add to Cart'
  },
  es: {
    welcome: 'Bienvenido',
    goodbye: 'Adiós',
    addToCart: 'Añadir al carrito'
  }
};

function LanguageProvider({ children }) {
  const [language, setLanguage] = useState('en');
  
  const t = (key) => translations[language][key] || key;
  
  return (
    <LanguageContext.Provider value={{ language, setLanguage, t }}>
      {children}
    </LanguageContext.Provider>
  );
}

// Usage
function WelcomeMessage() {
  const { t } = useContext(LanguageContext);
  return <h1>{t('welcome')}!</h1>;
}

Best Practices and Gotchas

✅ DO: Create Custom Hooks

// Good practice - encapsulate context logic
function useTheme() {
  const context = useContext(ThemeContext);
  if (!context) {
    throw new Error('useTheme must be used within ThemeProvider');
  }
  return context;
}

⚠️ AVOID: Context for Everything

Not everything needs to be in context. Use it for:

  • User authentication state
  • Theme/appearance preferences
  • Language settings
  • Shopping cart data

Don't use it for:

  • Form input values
  • UI state that affects only one component
  • Data that's only used in a small part of your app

🎯 Performance Tip: Split Contexts

// Instead of one giant context
const AppContext = createContext({
  user: null,
  theme: 'light',
  cart: [],
  notifications: []
});

// Split into focused contexts
const UserContext = createContext();
const ThemeContext = createContext();
const CartContext = createContext();
const NotificationContext = createContext();

Practice Exercises

Exercise 1: Create a Notification System

Build a context that manages toast notifications (success, error, info) that can be triggered from any component.

// Your task: Complete this NotificationProvider
function NotificationProvider({ children }) {
  // 1. Create state for notifications array
  // 2. Create addNotification function
  // 3. Create removeNotification function
  // 4. Auto-dismiss notifications after 3 seconds
  // 5. Provide value to children
}

Exercise 2: Settings Context

Create a context for user preferences that persists to localStorage:

  • Font size (small, medium, large)
  • Color scheme (light, dark, high-contrast)
  • Animations enabled/disabled

Exercise 3: Form Context

Build a multi-step form with context sharing form data between steps.

Key Takeaways