React - Authentication & Protected Routes
Overview
Estimated time: 20–30 minutes
Guard private pages behind authentication. Use a small auth context, a minimal hash router, and a ProtectedRoute component that redirects to login and back.
Try it: Guarded Dashboard
View source
const { createContext, useContext, useEffect, useMemo, useState } = React;
// Tiny router
function useHash(){
const [hash, setHash] = useState(() => window.location.hash || '#/home');
useEffect(() => { const on = () => setHash(window.location.hash || '#/home'); window.addEventListener('hashchange', on); return () => window.removeEventListener('hashchange', on); }, []);
return hash.replace('#','') || '/home';
}
function navigate(path){ window.location.hash = path; }
// Auth context
const AuthCtx = createContext(null);
function useAuth(){ return useContext(AuthCtx); }
function ProtectedRoute({ children }){
const { authed } = useAuth();
const path = useHash();
if (!authed){ navigate('/login?redirect='+encodeURIComponent(path)); return null; }
return children;
}
function Login(){
const { login } = useAuth();
const [pending, setPending] = useState(false);
const path = useHash();
const redirect = useMemo(() => {
const q = path.split('?')[1] || ''; const params = new URLSearchParams(q); return params.get('redirect') || '/home';
}, [path]);
async function onLogin(){ setPending(true); setTimeout(() => { login(); navigate(redirect); }, 600); }
return (
<div>
<h3>Login</h3>
<p>Hint: no credentials required in this demo.</p>
<button onClick={onLogin} disabled={pending}>{pending ? 'Logging in…' : 'Login'}</button>
</div>
);
}
function Home(){ return <div><h3>Home</h3><p>Public content.</p></div>; }
function Dashboard(){ const { logout } = useAuth(); return (<div><h3>Dashboard (Private)</h3><p>Welcome!</p><button onClick={logout}>Logout</button></div>); }
function App(){
const [authed, setAuthed] = useState(false);
const value = useMemo(() => ({ authed, login: () => setAuthed(true), logout: () => { setAuthed(false); navigate('/home'); } }), [authed]);
const path = useHash();
return (
<AuthCtx.Provider value={value}>
<nav style={{display:'flex', gap:8}}>
<a href="#/home">Home</a> | <a href="#/dashboard">Dashboard</a> | <span>Authed: {String(authed)}</span>
</nav>
<div style={{marginTop:8}}>
{path.startsWith('/login') && <Login />}
{path === '/home' && <Home />}
{path === '/dashboard' && (
<ProtectedRoute>
<Dashboard />
</ProtectedRoute>
)}
</div>
</AuthCtx.Provider>
);
}
ReactDOM.createRoot(document.getElementById('try-auth')).render(<App />);
Syntax primer
- Guard with a ProtectedRoute component that redirects if not authenticated.
- Preserve the redirect path in the URL and navigate back after login.
Common pitfalls
- Allowing access before auth is known—gate behind a known loading state in real apps.
- Dropping the intended destination—preserve path or params via redirect query.
Checks for Understanding
- Why preserve the intended destination when redirecting to login?
- What are common places to store auth state in small apps?
Show answers
- So users return to where they intended after authenticating, improving UX.
- In memory via context (as shown); for persistence, cookies/localStorage plus proper security considerations.
Exercises
- Add a “remember me” toggle and persist auth state across reloads.
- Protect a route that requires a specific role (e.g., admin) and redirect others.
- Show a loading state while determining auth status at startup.