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
}
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.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.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 /> |