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

  1. Why preserve the intended destination when redirecting to login?
  2. What are common places to store auth state in small apps?
Show answers
  1. So users return to where they intended after authenticating, improving UX.
  2. In memory via context (as shown); for persistence, cookies/localStorage plus proper security considerations.

Exercises

  1. Add a “remember me” toggle and persist auth state across reloads.
  2. Protect a route that requires a specific role (e.g., admin) and redirect others.
  3. Show a loading state while determining auth status at startup.