import { createContext, useContext, useEffect, useMemo, useState, ReactNode } from 'react';
import { authApi, User } from './authApi';
type Status = 'loading' | 'authenticated' | 'anonymous';
interface AuthValue {
user: User | null;
status: Status;
signIn: (email: string, password: string) => Promise<void>;
signOut: () => Promise<void>;
}
const AuthContext = createContext<AuthValue | undefined>(undefined);
export function AuthProvider({ children }: { children: ReactNode }) {
const [user, setUser] = useState<User | null>(null);
const [status, setStatus] = useState<Status>('loading');
useEffect(() => {
let active = true;
authApi
.me()
.then((u) => active && (setUser(u), setStatus('authenticated')))
.catch(() => active && setStatus('anonymous'));
return () => {
active = false;
};
}, []);
const value = useMemo<AuthValue>(
() => ({
user,
status,
async signIn(email, password) {
const u = await authApi.login(email, password);
setUser(u);
setStatus('authenticated');
},
async signOut() {
await authApi.logout();
setUser(null);
setStatus('anonymous');
},
}),
[user, status],
);
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
}
export function useAuth(): AuthValue {
const ctx = useContext(AuthContext);
if (!ctx) throw new Error('useAuth must be used within an AuthProvider');
return ctx;
}
import { Navigate, Outlet, useLocation } from 'react-router-dom';
import { useAuth } from './AuthContext';
export function RequireAuth() {
const { user, status } = useAuth();
const location = useLocation();
if (status === 'loading') {
return <div className="auth-splash">Checking session…</div>;
}
if (!user) {
return <Navigate to="/login" replace state={{ from: location }} />;
}
return <Outlet />;
}
import { Routes, Route } from 'react-router-dom';
import { RequireAuth } from './RequireAuth';
import LoginPage from './LoginPage';
import Dashboard from './pages/Dashboard';
import Settings from './pages/Settings';
import PublicHome from './pages/PublicHome';
export default function AppRoutes() {
return (
<Routes>
<Route path="/" element={<PublicHome />} />
<Route path="/login" element={<LoginPage />} />
<Route element={<RequireAuth />}>
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/settings" element={<Settings />} />
</Route>
</Routes>
);
}
import { FormEvent, useState } from 'react';
import { useLocation, useNavigate } from 'react-router-dom';
import { useAuth } from './AuthContext';
interface RedirectState {
from?: { pathname: string };
}
export default function LoginPage() {
const { signIn } = useAuth();
const navigate = useNavigate();
const location = useLocation();
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [error, setError] = useState<string | null>(null);
const from = (location.state as RedirectState | null)?.from?.pathname ?? '/';
async function handleSubmit(e: FormEvent) {
e.preventDefault();
setError(null);
try {
await signIn(email, password);
navigate(from, { replace: true });
} catch {
setError('Invalid email or password');
}
}
return (
<form onSubmit={handleSubmit} className="login">
<h1>Sign in</h1>
{error && <p role="alert">{error}</p>}
<input value={email} onChange={(e) => setEmail(e.target.value)} type="email" placeholder="Email" required />
<input value={password} onChange={(e) => setPassword(e.target.value)} type="password" placeholder="Password" required />
<button type="submit">Continue</button>
</form>
);
}
This snippet shows the standard React pattern for gating routes behind authentication using a context provider plus a wrapper component, rather than sprinkling auth checks inside every page. The idea is to centralize "who is the user and are we still figuring that out" in one place, then let route guards consume that single source of truth.
In AuthContext.tsx, an AuthContext holds the current user, a status flag, and signIn/signOut methods. The AuthProvider starts in a loading status and runs an effect on mount that calls authApi.me() to restore an existing session from an httpOnly cookie. This bootstrap step is important: without it, a page refresh would momentarily treat an authenticated user as logged out and bounce them to the login screen. The value is memoized with useMemo so consumers don't re-render on every provider render. The exported useAuth hook throws when used outside the provider, which turns a silent undefined bug into a loud, immediate error during development.
In RequireAuth.tsx, the guard reads status and user from useAuth. While status === 'loading' it renders a fallback instead of redirecting — this prevents the refresh flicker described above. When there is no user, it renders React Router's <Navigate> to /login, passing the current location in state so the login page can send the user back where they intended to go. replace is used so the protected URL doesn't linger in browser history. When a user exists it renders <Outlet />, letting nested routes proceed.
In AppRoutes.tsx, RequireAuth wraps a parent <Route> whose children inherit the guard, so the protected subtree is declared once. LoginPage.tsx demonstrates the redirect-back flow: after signIn succeeds it reads location.state.from and navigates there, defaulting to /.
The main trade-off is that this is client-side enforcement only — it improves UX but the server must still authorize every request. The pattern's strength is separation: authentication state lives in context, redirect policy lives in the guard, and route structure stays declarative.
Related snips
payload = {
sub: user.id,
iss: 'https://auth.example.com',
aud: 'codesnips-api',
exp: 15.minutes.from_now.to_i,
iat: Time.now.to_i,
JWT issuance and verification without common footguns
export interface RetryOptions {
retries: number;
baseMs: number;
maxMs: number;
signal?: AbortSignal;
onRetry?: (attempt: number, delay: number, err: unknown) => void;
Exponential backoff with jitter for retries
import axios, { AxiosError } from 'axios'
import { v4 as uuidv4 } from 'uuid'
const api = axios.create({
baseURL: import.meta.env.VITE_API_URL || 'http://localhost:3000/api/v1',
timeout: 15000,
Axios API client with interceptors
import { create } from 'zustand'
import { persist } from 'zustand/middleware'
interface FilterState {
search: string
category: string | null
Zustand for lightweight state management
import { randomBytes, createHash } from "crypto";
import jwt from "jsonwebtoken";
import { RefreshTokenStore } from "./store";
const ACCESS_SECRET = process.env.ACCESS_SECRET!;
const ACCESS_TTL = "15m";
JWT access + refresh token rotation (conceptual)
import React from "react";
type FallbackProps = {
error: Error;
reset: () => void;
};
React Error Boundary + error reporting hook
Share this code
Here's the card — post it anywhere.