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.