🔗 useContext Hook

Share Data Without Prop Drilling

What is Context?

Context provides a way to pass data through the component tree without having to pass props down manually at every level. Perfect for global data like themes, user info, language settings.

The Problem: Prop Drilling

// Passing props through many levels
<App user={user}>
  <Dashboard user={user}>
    <Sidebar user={user}>
      <UserMenu user={user} />
    </Sidebar>
  </Dashboard>
</App>

// Solution: Context!
// UserMenu can access user directly

📚 Basic Setup

Step 1: Create Context

import { createContext } from 'react';

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

Step 2: Provide Context

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

Step 3: Consume Context

import { useContext } from 'react';

function ThemedButton() {
  const theme = useContext(ThemeContext);
  
  return (
    <button className={`btn-${theme}`}>
      I'm {theme} themed!
    </button>
  );
}

🎨 Complete Theme Example

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

const ThemeContext = createContext();

export function ThemeProvider({ children }) {
  const [theme, setTheme] = useState('light');
  
  const toggleTheme = () => {
    setTheme(prev => prev === 'light' ? 'dark' : 'light');
  };
  
  return (
    <ThemeContext.Provider value={{ theme, toggleTheme }}>
      {children}
    </ThemeContext.Provider>
  );
}

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

// App.js
function App() {
  return (
    <ThemeProvider>
      <Header />
      <Main />
      <Footer />
    </ThemeProvider>
  );
}

// Any component can use theme
function Header() {
  const { theme, toggleTheme } = useTheme();
  
  return (
    <header className={theme}>
      <h1>My App</h1>
      <button onClick={toggleTheme}>
        Toggle {theme === 'light' ? '🌙' : '☀️'}
      </button>
    </header>
  );
}

👤 User Authentication Context

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

const AuthContext = createContext();

export function AuthProvider({ children }) {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);
  
  // Check if user is logged in on mount
  useEffect(() => {
    const checkAuth = async () => {
      try {
        const response = await fetch('/api/me');
        const data = await response.json();
        setUser(data);
      } catch (error) {
        setUser(null);
      } finally {
        setLoading(false);
      }
    };
    checkAuth();
  }, []);
  
  const login = async (email, password) => {
    const response = await fetch('/api/login', {
      method: 'POST',
      body: JSON.stringify({ email, password })
    });
    const data = await response.json();
    setUser(data.user);
    return data;
  };
  
  const logout = async () => {
    await fetch('/api/logout', { method: 'POST' });
    setUser(null);
  };
  
  const value = {
    user,
    loading,
    login,
    logout,
    isAuthenticated: !!user
  };
  
  return (
    <AuthContext.Provider value={value}>
      {children}
    </AuthContext.Provider>
  );
}

export function useAuth() {
  return useContext(AuthContext);
}

// Usage in components
function Navbar() {
  const { user, logout, isAuthenticated } = useAuth();
  
  if (!isAuthenticated) {
    return <Link to="/login">Login</Link>;
  }
  
  return (
    <div>
      <span>Welcome, {user.name}!</span>
      <button onClick={logout}>Logout</button>
    </div>
  );
}

function ProtectedRoute({ children }) {
  const { isAuthenticated, loading } = useAuth();
  
  if (loading) return <div>Loading...</div>;
  if (!isAuthenticated) return <Navigate to="/login" />;
  
  return children;
}

🌐 Multiple Contexts

// Combine multiple providers
function App() {
  return (
    <AuthProvider>
      <ThemeProvider>
        <LanguageProvider>
          <NotificationProvider>
            <Router>
              <Routes />
            </Router>
          </NotificationProvider>
        </LanguageProvider>
      </ThemeProvider>
    </AuthProvider>
  );
}

// Use multiple contexts in one component
function UserProfile() {
  const { user } = useAuth();
  const { theme } = useTheme();
  const { t } = useLanguage();
  const { notify } = useNotification();
  
  return (
    <div className={`profile ${theme}`}>
      <h2>{t('welcome', { name: user.name })}</h2>
      <button onClick={() => notify('Profile updated!')}>
        {t('save')}
      </button>
    </div>
  );
}

🛒 Shopping Cart Context

// CartContext.js
const CartContext = createContext();

export function CartProvider({ children }) {
  const [items, setItems] = useState([]);
  
  const addToCart = (product) => {
    setItems(prev => {
      const existing = prev.find(item => item.id === product.id);
      if (existing) {
        return prev.map(item =>
          item.id === product.id
            ? { ...item, quantity: item.quantity + 1 }
            : item
        );
      }
      return [...prev, { ...product, quantity: 1 }];
    });
  };
  
  const removeFromCart = (productId) => {
    setItems(prev => prev.filter(item => item.id !== productId));
  };
  
  const updateQuantity = (productId, quantity) => {
    if (quantity <= 0) {
      removeFromCart(productId);
      return;
    }
    setItems(prev =>
      prev.map(item =>
        item.id === productId ? { ...item, quantity } : item
      )
    );
  };
  
  const clearCart = () => setItems([]);
  
  const total = items.reduce((sum, item) => 
    sum + item.price * item.quantity, 0
  );
  
  const itemCount = items.reduce((sum, item) => 
    sum + item.quantity, 0
  );
  
  const value = {
    items,
    addToCart,
    removeFromCart,
    updateQuantity,
    clearCart,
    total,
    itemCount
  };
  
  return (
    <CartContext.Provider value={value}>
      {children}
    </CartContext.Provider>
  );
}

export function useCart() {
  return useContext(CartContext);
}

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

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

function Checkout() {
  const { items, total, clearCart } = useCart();
  
  return (
    <div>
      {items.map(item => (
        <div key={item.id}>
          {item.name} x {item.quantity} = ${item.price * item.quantity}
        </div>
      ))}
      <h3>Total: ${total.toFixed(2)}</h3>
      <button onClick={clearCart}>Clear Cart</button>
    </div>
  );
}

⚡ Performance Optimization

Problem: Unnecessary Re-renders

// ❌ Creates new object every render
function Provider({ children }) {
  const [count, setCount] = useState(0);
  
  // New object on every render!
  return (
    <Context.Provider value={{ count, setCount }}>
      {children}
    </Context.Provider>
  );
}

// ✅ Memoize the value
function Provider({ children }) {
  const [count, setCount] = useState(0);
  
  const value = useMemo(
    () => ({ count, setCount }),
    [count]
  );
  
  return (
    <Context.Provider value={value}>
      {children}
    </Context.Provider>
  );
}

Split Contexts for Better Performance:

// ❌ One big context
const AppContext = createContext();
// Changes to any value re-render all consumers

// ✅ Split into smaller contexts
const UserContext = createContext();
const ThemeContext = createContext();
const SettingsContext = createContext();
// Only relevant consumers re-render

⚠️ Common Mistakes

1. Using Context Outside Provider

// Error: useContext returns undefined
function Component() {
  const value = useContext(MyContext);
  // value is undefined if no Provider above
}

// ✅ Always check
export function useMyContext() {
  const context = useContext(MyContext);
  if (context === undefined) {
    throw new Error('useMyContext must be used within Provider');
  }
  return context;
}

2. Not Memoizing Context Value

// ❌ New object every render
<Context.Provider value={{ user, setUser }}>

// ✅ Memoize
const value = useMemo(() => ({ user, setUser }), [user]);

🎯 Key Takeaways