React

Context API

Share state across components without prop drilling using React Context API.

What is Context?

Context provides a way to pass data through the component tree without having to pass props down manually at every level. It's ideal for global state like themes, user authentication, and language settings.

Context is not a replacement for proper state management. For complex state logic with many updates, consider Zustand or Redux Toolkit.

Creating and Using Context

The Context API consists of three parts: creating the context, providing values, and consuming them in child components.

'use client';

import { createContext, useContext, useState, ReactNode } from 'react';

// 1. Define the context type
interface ThemeContextType {
  theme: 'light' | 'dark';
  toggleTheme: () => void;
  primary: string;
}

// 2. Create context with default value
const ThemeContext = createContext<ThemeContextType | null>(null);

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

// 4. Create the Provider component
export function ThemeProvider({ children }: { children: ReactNode }) {
  const [theme, setTheme] = useState<'light' | 'dark'>('dark');

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

  const primary = theme === 'dark' ? '#aec6ff' : '#0066cc';

  return (
    <ThemeContext.Provider value={{ theme, toggleTheme, primary }}>
      {children}
    </ThemeContext.Provider>
  );
}

// 5. Using the context in any child component
function ThemedButton({ children }: { children: ReactNode }) {
  const { theme, toggleTheme, primary } = useTheme();

  return (
    <button
      onClick={toggleTheme}
      style={{ backgroundColor: primary, color: theme === 'dark' ? '#000' : '#fff' }}
    >
      {children} (Current: {theme})
    </button>
  );
}

Authentication Context Pattern

A common real-world use of Context is managing authentication state across your entire application.

'use client';

import { createContext, useContext, useState, useEffect, ReactNode } from 'react';

interface User {
  id: string;
  name: string;
  email: string;
  role: 'admin' | 'user';
}

interface AuthContextType {
  user: User | null;
  isLoading: boolean;
  login: (email: string, password: string) => Promise<void>;
  logout: () => void;
  isAuthenticated: boolean;
}

const AuthContext = createContext<AuthContextType | null>(null);

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

export function AuthProvider({ children }: { children: ReactNode }) {
  const [user, setUser] = useState<User | null>(null);
  const [isLoading, setIsLoading] = useState(true);

  useEffect(() => {
    // Check for existing session on mount
    async function checkSession() {
      try {
        const res = await fetch('/api/auth/me');
        if (res.ok) {
          const userData = await res.json();
          setUser(userData);
        }
      } catch {
        // No session
      } finally {
        setIsLoading(false);
      }
    }
    checkSession();
  }, []);

  const login = async (email: string, password: string) => {
    const res = await fetch('/api/auth/login', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ email, password }),
    });

    if (!res.ok) throw new Error('Invalid credentials');

    const userData = await res.json();
    setUser(userData);
  };

  const logout = () => {
    fetch('/api/auth/logout', { method: 'POST' });
    setUser(null);
  };

  return (
    <AuthContext.Provider value={{
      user, isLoading, login, logout,
      isAuthenticated: !!user,
    }}>
      {children}
    </AuthContext.Provider>
  );
}