import { createContext, useContext, useEffect, useMemo, useState } from 'react';
import { fetchCurrentUser, postLogin, postLogout } from './api';
const AuthContext = createContext(null);
export function AuthProvider({ children }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
let active = true;
fetchCurrentUser()
.then((u) => { if (active) setUser(u); })
.catch(() => { if (active) setUser(null); })
.finally(() => { if (active) setLoading(false); });
return () => { active = false; };
}, []);
const value = useMemo(() => ({
user,
loading,
async login(credentials) {
const u = await postLogin(credentials);
setUser(u);
return u;
},
async logout() {
await postLogout();
setUser(null);
},
}), [user, loading]);
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
}
export function useAuth() {
const ctx = useContext(AuthContext);
if (ctx === null) {
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 default function RequireAuth() {
const { user, loading } = useAuth();
const location = useLocation();
if (loading) {
return <div className="auth-splash">Checking session\u2026</div>;
}
if (!user) {
return (
<Navigate
to="/login"
replace
state={{ from: location.pathname + location.search }}
/>
);
}
return <Outlet />;
}
import { BrowserRouter, Routes, Route } from 'react-router-dom';
import { AuthProvider } from './AuthContext';
import RequireAuth from './RequireAuth';
import LoginPage from './pages/LoginPage';
import Dashboard from './pages/Dashboard';
import Settings from './pages/Settings';
import NotFound from './pages/NotFound';
export default function App() {
return (
<BrowserRouter>
<AuthProvider>
<Routes>
<Route path="/login" element={<LoginPage />} />
<Route element={<RequireAuth />}>
<Route path="/" element={<Dashboard />} />
<Route path="/settings" element={<Settings />} />
</Route>
<Route path="*" element={<NotFound />} />
</Routes>
</AuthProvider>
</BrowserRouter>
);
}
import { useState } from 'react';
import { useNavigate, useLocation } from 'react-router-dom';
import { useAuth } from './AuthContext';
export default function LoginPage() {
const { login } = useAuth();
const navigate = useNavigate();
const location = useLocation();
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [error, setError] = useState(null);
const redirectTo = location.state?.from || '/';
async function handleSubmit(event) {
event.preventDefault();
setError(null);
try {
await login({ email, password });
navigate(redirectTo, { replace: true });
} catch (err) {
setError('Invalid email or password');
}
}
return (
<form onSubmit={handleSubmit} className="login-form">
{error && <p role="alert">{error}</p>}
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="Email"
required
/>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="Password"
required
/>
<button type="submit">Sign in</button>
</form>
);
}
This snippet shows the standard React way to gate private pages behind authentication without scattering session checks through every component. The design has three parts: a context that owns auth state, a route wrapper that enforces it, and the routing table that composes them.
In AuthContext.jsx, an AuthProvider holds the current user and a loading flag so the app can distinguish "not logged in" from "we don't know yet". On mount it calls fetchCurrentUser() to rehydrate the session from an existing cookie, which matters on a hard refresh where in-memory state is gone. The login and logout functions update state after talking to the API, and the whole value is memoized with useMemo so consumers don't re-render on unrelated parent updates. The useAuth hook wraps useContext and throws when used outside the provider, turning a subtle null-access bug into a loud, immediate error.
The loading flag is the key edge case. Without it, a protected route would briefly see user === null during the async fetch and redirect a genuinely logged-in user to the login page — an annoying flash. RequireAuth.jsx handles this by returning a fallback while loading is true, and only then deciding. When there is no user it renders react-router's Navigate with replace so the login page doesn't pollute the history stack, and it stashes the attempted URL in location.state. That captured from is what lets the login flow send the user back to where they were headed.
When a user is present, RequireAuth renders <Outlet />, which means it works as a layout route wrapping many children rather than guarding one element at a time. App.jsx demonstrates this: public routes like /login sit outside the guard, while everything under the RequireAuth element is protected in one place.
This pattern centralizes the redirect logic, keeps individual pages ignorant of auth, and composes cleanly with nested routes. The trade-off is that it trusts client state for UX only — the server must still authorize every request, since anyone can bypass a client-side guard.
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
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
raw_token = SecureRandom.urlsafe_base64(32)
token_digest = Digest::SHA256.hexdigest(raw_token)
PasswordReset.create!(
user: user,
token_digest: token_digest,
Secure random token generation for sessions and recovery flows
Share this code
Here's the card — post it anywhere.