Building the AuthContext — User State Across the App

The AuthContext is the most important context in the MERN Blog — it is consumed by the Header (to show login/logout), ProtectedRoute (to guard pages), PostCard (to show edit/delete controls to the owner), and any component that needs to know who the current user is. Building it correctly means: reading the stored token from localStorage on app load, verifying it with the Express API before trusting it, exposing stable login/logout/register functions, and making the loading state available so ProtectedRoute can show a spinner instead of incorrectly redirecting.

The AuthContext Shape

// What AuthContext exposes to consumers
{
  user:     null | { _id, name, email, role, avatar },
  token:    null | 'eyJhbGciOi...',
  loading:  true | false,   // true while verifying token on app load
  login:    async (email, password) => void,
  register: async (name, email, password) => void,
  logout:   () => void,
  updateUser: (updatedUser) => void, // after profile update
}
Note: The loading state in AuthContext is critical. When the app loads for the first time, it reads a token from localStorage and must verify it with the Express API (GET /api/auth/me) before deciding if the user is logged in. During this verification, loading is true. ProtectedRoute must wait for loading === false before redirecting — otherwise it redirects logged-in users to the login page during the brief verification window.
Tip: Store the token in localStorage for persistence but treat it as a cache — always verify it server-side on app load. A token in localStorage can be expired, revoked, or tampered with. The GET /api/auth/me endpoint confirms the token is still valid and returns the current user data. If the request fails with a 401, clear the token and set user to null.
Warning: Never store sensitive data directly in AuthContext state that you pass around as props — specifically, avoid exposing the raw token to every consuming component. Components that need to make API calls should use the configured Axios instance (from Chapter 20 Lesson 2) which reads the token from localStorage via the interceptor. Only the AuthContext itself needs to manage the token in localStorage.

The Complete AuthContext

// src/context/AuthContext.jsx
import { createContext, useContext, useState, useEffect, useCallback, useMemo } from 'react';
import api from '@/services/api';

const AuthContext = createContext(undefined);

export function AuthProvider({ children }) {
  const [user,    setUser]    = useState(null);
  const [loading, setLoading] = useState(true); // true until initial token check done

  // ── On mount: verify any stored token ─────────────────────────────────────
  useEffect(() => {
    const verifyToken = async () => {
      const token = localStorage.getItem('token');
      if (!token) {
        setLoading(false); // no token → definitely not logged in
        return;
      }
      try {
        const res = await api.get('/auth/me');
        setUser(res.data.data); // token valid → set user
      } catch {
        // Token invalid or expired → clear it
        localStorage.removeItem('token');
        setUser(null);
      } finally {
        setLoading(false); // verification complete
      }
    };
    verifyToken();
  }, []); // runs once on mount

  // ── Login ──────────────────────────────────────────────────────────────────
  const login = useCallback(async (email, password) => {
    const res = await api.post('/auth/login', { email, password });
    localStorage.setItem('token', res.data.token);
    setUser(res.data.data);
    // Callers handle errors — login throws on failure
  }, []);

  // ── Register ───────────────────────────────────────────────────────────────
  const register = useCallback(async (name, email, password) => {
    const res = await api.post('/auth/register', { name, email, password });
    localStorage.setItem('token', res.data.token);
    setUser(res.data.data);
  }, []);

  // ── Logout ─────────────────────────────────────────────────────────────────
  const logout = useCallback(() => {
    localStorage.removeItem('token');
    setUser(null);
  }, []);

  // ── Update user (after profile edit) ───────────────────────────────────────
  const updateUser = useCallback((updatedUser) => {
    setUser(prev => ({ ...prev, ...updatedUser }));
  }, []);

  const value = useMemo(() => ({
    user, loading, login, register, logout, updateUser,
    isAuthenticated: !!user,
  }), [user, loading, login, register, logout, updateUser]);

  return (
    <AuthContext.Provider value={value}>
      {children}
    </AuthContext.Provider>
  );
}

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

Using AuthContext in Components

// ── Header: show user name or login link ───────────────────────────────────────
import { useAuth } from '@/context/AuthContext';

function Header() {
  const { user, logout, isAuthenticated } = useAuth();

  return (
    <header>
      <Link to="/">MERN Blog</Link>
      <nav>
        {isAuthenticated ? (
          <>
            <span>Hi, {user.name}</span>
            <Link to="/dashboard">Dashboard</Link>
            <button onClick={logout}>Log Out</button>
          </>
        ) : (
          <>
            <Link to="/login">Log In</Link>
            <Link to="/register">Register</Link>
          </>
        )}
      </nav>
    </header>
  );
}

// ── LoginPage: call login() from context ──────────────────────────────────────
function LoginPage() {
  const { login }   = useAuth();
  const navigate    = useNavigate();
  const location    = useLocation();
  const [error, setError] = useState('');

  const handleSubmit = async (e) => {
    e.preventDefault();
    try {
      await login(formData.email, formData.password);
      navigate(location.state?.from || '/dashboard', { replace: true });
    } catch (err) {
      setError(err.response?.data?.message || 'Login failed');
    }
  };
  // ...
}

// ── ProtectedRoute: wait for loading, then check user ─────────────────────────
function ProtectedRoute({ children }) {
  const { user, loading } = useAuth();
  const location = useLocation();

  if (loading) return <Spinner message="Checking authentication..." />;
  if (!user)   return <Navigate to="/login" state={{ from: location.pathname }} replace />;
  return children;
}

// ── PostCard: show edit/delete only to the owner ──────────────────────────────
function PostCard({ post, onDelete }) {
  const { user } = useAuth();
  const isOwner  = user && (user._id === post.author?._id || user.role === 'admin');
  return (
    <article>
      <h2>{post.title}</h2>
      {isOwner && <button onClick={() => onDelete(post._id)}>Delete</button>}
    </article>
  );
}

Common Mistakes

Mistake 1 — Not verifying the token on app load

❌ Wrong — trusting the token in localStorage without server verification:

useEffect(() => {
  const token = localStorage.getItem('token');
  if (token) setUser(decodeJWT(token)); // decoded but not verified!
  setLoading(false);
  // An expired or tampered token would make the user appear logged in
}, []);

✅ Correct — verify with the server before trusting the token:

useEffect(() => {
  const verify = async () => {
    const token = localStorage.getItem('token');
    if (!token) { setLoading(false); return; }
    try { const res = await api.get('/auth/me'); setUser(res.data.data); }
    catch { localStorage.removeItem('token'); }
    finally { setLoading(false); }
  };
  verify();
}, []);

Mistake 2 — Forgetting to handle the loading state in ProtectedRoute

❌ Wrong — user briefly sees the login page flash before auth is verified:

function ProtectedRoute({ children }) {
  const { user } = useAuth();
  if (!user) return <Navigate to="/login" replace />;
  return children;
  // user is null while loading → always redirects on first load!
}

✅ Correct — show spinner while loading:

if (loading) return <Spinner />; // ✓ wait for auth check
if (!user)   return <Navigate to="/login" replace />;

Mistake 3 — Not clearing the token on logout

❌ Wrong — logout only clears user state but leaves token in localStorage:

const logout = () => setUser(null); // token still in localStorage!
// Next app load: token found → API called → if valid, user is logged back in

✅ Correct — always remove the token on logout:

const logout = () => { localStorage.removeItem('token'); setUser(null); }; // ✓

Quick Reference

Task Code
Verify token on mount useEffect(() => { api.get('/auth/me').then(setUser) }, [])
Login action POST to /auth/login → setToken + setUser
Logout action localStorage.removeItem(‘token’) + setUser(null)
Consume in component const { user, login, logout } = useAuth()
Check ownership user?._id === post.author?._id
Guard route if (loading) return <Spinner />; if (!user) return <Navigate />

🧠 Test Yourself

A logged-in user refreshes the page on /dashboard. Without the token verification useEffect, what would happen and why?