Redirects (Auth)

This example has 3 pages: a public page, a protected page, and a login screen. To see the protected page, you must first login. First, visit the public page. Then, visit the protected page. You're not yet logged in, so you are redirected to the login page. After you login, you are redirected back to the protected page. Notice how the URL changes each time. If you click the back button at this point, you will not go back to the login page, as you're already logged in.

The old "PrivateRoute" wrapper worked by wrapping <Route render={...}>, which no longer exists. The current equivalent is a layout route: a plain component rendered as the "element" of a parent <Route>, which renders <Outlet /> (letting its matched child route through) when authenticated, or <Navigate> to the login page otherwise.


import React, { useContext, createContext, useState } from "react";
import {
  BrowserRouter as Router, Routes, Route, Link, Navigate, Outlet,
  useNavigate, useLocation,
} from "react-router";

export default function AuthExample() {
  return (
    <ProvideAuth>
      <Router>
        <div>
          <AuthButton />
          <ul>
            <li><Link to="/public">Public Page</Link></li>
            <li><Link to="/protected">Protected Page</Link></li>
          </ul>
          <Routes>
            <Route path="/public" element={<PublicPage />} />
            <Route path="/login" element={<LoginPage />} />
            <Route element={<RequireAuth />}>
              <Route path="/protected" element={<ProtectedPage />} />
            </Route>
          </Routes>
        </div>
      </Router>
    </ProvideAuth>
  );
}
const fakeAuth = {
  isAuthenticated: false,
  signin(cb) { fakeAuth.isAuthenticated = true; setTimeout(cb, 100); /* fake async */ },
  signout(cb) { fakeAuth.isAuthenticated = false; setTimeout(cb, 100); },
};
const authContext = createContext();
function ProvideAuth({ children }) {
  const auth = useProvideAuth();
  return (<authContext.Provider value={auth}>{children}</authContext.Provider>);
}
function useAuth() { return useContext(authContext); }
function useProvideAuth() {
  const [user, setUser] = useState(null);
  const signin = (cb) => fakeAuth.signin(() => { setUser("user"); cb(); });
  const signout = (cb) => fakeAuth.signout(() => { setUser(null); cb(); });
  return { user, signin, signout };
}
function AuthButton() {
  let navigate = useNavigate();
  let auth = useAuth();
  return auth.user ? (
    <p>
      Welcome!{" "}
      <button onClick={() => { auth.signout(() => navigate("/")); }}>
        Sign out
      </button>
    </p>
  ) : (<p>You are not logged in.</p>);
}
// A layout route guarding everything nested inside it.
function RequireAuth() {
  let auth = useAuth();
  let location = useLocation();
  if (!auth.user) {
    // Remember where we were headed, and replace so the guarded page
    // never ends up in history underneath the login page.
    return <Navigate to="/login" state={{ from: location }} replace />;
  }
  return <Outlet />;
}
function PublicPage() { return <h3>Public</h3>; }
function ProtectedPage() { return <h3>Protected</h3>; }
function LoginPage() {
  let navigate = useNavigate();
  let location = useLocation();
  let auth = useAuth();
  let from = location.state?.from?.pathname || "/";
  let login = () => {
    auth.signin(() => { navigate(from, { replace: true }); });
  };
  return (
    <div>
      <p>You must log in to view the page at {from}</p>
      <button onClick={login}>Log in</button>
    </div>
  );
}