Auth Context — Global Authentication State

Authentication state — who is logged in, their tokens, and the login/logout actions — needs to be accessible from many parts of the blog application: the router (protected routes), the Axios interceptor (attach the token), the header (show user name), and every protected page. An AuthContext provides all of this from a single provider that wraps the entire app. On startup, it validates any token stored in localStorage by calling /api/users/me, populating the context if the token is still valid.

Auth Context Implementation

// src/context/AuthContext.jsx
import { createContext, useContext, useState, useEffect, useMemo, useCallback } from "react";
import { authApi }  from "@/services/auth";
import { postsApi } from "@/services/posts";   // for Axios interceptor update

const AuthContext = createContext(null);

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

export function AuthProvider({ children }) {
    const [user,      setUser]      = useState(null);
    const [isLoading, setIsLoading] = useState(true);   // checking stored token

    // ── On mount: validate stored token ──────────────────────────────────────
    useEffect(() => {
        const token = localStorage.getItem("access_token");
        if (!token) { setIsLoading(false); return; }

        authApi.me()
            .then((userData) => setUser(userData))
            .catch(() => {
                localStorage.removeItem("access_token");
                localStorage.removeItem("refresh_token");
            })
            .finally(() => setIsLoading(false));
    }, []);

    // ── Actions ────────────────────────────────────────────────────────────────
    const login = useCallback(async (email, password) => {
        const { access_token, refresh_token } = await authApi.login(email, password);
        localStorage.setItem("access_token",  access_token);
        localStorage.setItem("refresh_token", refresh_token);
        const userData = await authApi.me();
        setUser(userData);
        return userData;
    }, []);

    const logout = useCallback(async () => {
        const refreshToken = localStorage.getItem("refresh_token");
        try { await authApi.logout(refreshToken); } catch { /* ignore */ }
        localStorage.removeItem("access_token");
        localStorage.removeItem("refresh_token");
        setUser(null);
    }, []);

    const register = useCallback(async (data) => {
        await authApi.register(data);
        return login(data.email, data.password);   // auto-login after register
    }, [login]);

    // Memoize to prevent re-renders on every AuthProvider render
    const value = useMemo(() => ({
        user,
        isLoggedIn: Boolean(user),
        isLoading,
        login,
        logout,
        register,
    }), [user, isLoading, login, logout, register]);

    return (
        <AuthContext.Provider value={value}>
            {children}
        </AuthContext.Provider>
    );
}
Note: The isLoading state during the initial token validation is critical. Without it, the app has a brief moment where isLoggedIn is false (because the user check has not completed) even for authenticated users. This causes authenticated users to see the login redirect flash before the user data loads. The ProtectedRoute from Chapter 37 uses this isLoading flag to show a spinner instead of redirecting prematurely.
Tip: Wrap the login, logout, and register functions in useCallback with empty dependency arrays ([]). These functions do not depend on any state (they read from localStorage directly and call setUser via the setter reference). Without useCallback, they are recreated on every AuthProvider render, causing the memoized context value to change on every render (since functions are compared by reference), defeating the purpose of useMemo.
Warning: Storing tokens in localStorage is susceptible to XSS attacks — any malicious script injected into your page can read them. Always sanitise any user-generated HTML before rendering it (use DOMPurify), set a strict Content Security Policy, and keep access token lifetimes short (15 minutes). For applications handling sensitive data, consider httpOnly cookies as an alternative — they cannot be read by JavaScript at all.

Using AuthContext in Components

// src/pages/LoginPage.jsx
import { useAuth }     from "@/context/AuthContext";
import { useNavigate, useLocation } from "react-router-dom";

export default function LoginPage() {
    const { login } = useAuth();
    const navigate  = useNavigate();
    const location  = useLocation();
    const from = location.state?.from?.pathname ?? "/dashboard";
    const [error, setError]       = useState(null);
    const [isLoading, setLoading] = useState(false);

    async function handleSubmit(e) {
        e.preventDefault();
        const form = new FormData(e.target);
        setLoading(true);
        try {
            await login(form.get("email"), form.get("password"));
            navigate(from, { replace: true });
        } catch (err) {
            setError(err.response?.status === 401
                ? "Invalid email or password"
                : "Login failed — please try again");
        } finally {
            setLoading(false);
        }
    }
    // ...
}

// src/components/layout/Header.jsx
import { useAuth } from "@/context/AuthContext";

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

    return (
        <header>
            {isLoggedIn ? (
                <>
                    <span>{user.name}</span>
                    <button onClick={logout}>Sign Out</button>
                </>
            ) : (
                <a href="/login">Sign In</a>
            )}
        </header>
    );
}

Wiring AuthProvider in main.jsx

// src/main.jsx
import { StrictMode }    from "react";
import { createRoot }    from "react-dom/client";
import { BrowserRouter } from "react-router-dom";
import { AuthProvider }  from "@/context/AuthContext";
import App               from "./App.jsx";

createRoot(document.getElementById("root")).render(
    <StrictMode>
        <BrowserRouter>
            <AuthProvider>    {/* ← wraps everything */}
                <App />
            </AuthProvider>
        </BrowserRouter>
    </StrictMode>
);

Common Mistakes

Mistake 1 — Not handling the isLoading state (auth flash)

❌ Wrong — unauthenticated redirect fires before token check completes:

function ProtectedRoute() {
    const { isLoggedIn } = useAuth();   // initially false for everyone!
    if (!isLoggedIn) return <Navigate to="/login" />;   // flashes login!
}

✅ Correct — check isLoading first (as covered in Chapter 37).

Mistake 2 — Not memoizing context value (all consumers re-render too often)

❌ Wrong — new object on every AuthProvider render:

return <AuthContext.Provider value={{ user, login, logout }}>;

✅ Correct — use useMemo.

Quick Reference

Hook Return Type Purpose
user User | null Current user object (null if logged out)
isLoggedIn boolean Shorthand for Boolean(user)
isLoading boolean True during initial token validation
login(email, password) async fn Authenticate and store tokens
logout() async fn Clear tokens and user state
register(data) async fn Create account and auto-login

🧠 Test Yourself

A user refreshes the page. The AuthProvider mounts. What happens in the useEffect before isLoading becomes false?