Notifications

No notifications

/Phase 3

React Router

React Router β€” Client-Side Navigation

React Router enables single-page application (SPA) navigation without full page reloads. It maps URL paths to React components and keeps the UI in sync with the browser's address bar.

Core Concepts

ConceptDescription
Wraps your app, enables routing via History API
Container that renders the first matching
Maps a URL path to a component
Navigates without page reload (replaces )
Renders child routes inside a layout

Basic Setup

<BrowserRouter>
  <Routes>
    <Route path="/" element={<Home />} />
    <Route path="/about" element={<About />} />
    <Route path="/users/:id" element={<UserProfile />} />
    <Route path="*" element={<NotFound />} />
  </Routes>
</BrowserRouter>

Dynamic Routes

Use :param in the path and read it with useParams():

const { id } = useParams(); // from path="/users/:id"

Nested Layouts

<Route path="/dashboard" element={<DashboardLayout />}>
  <Route index element={<Overview />} />
  <Route path="settings" element={<Settings />} />
</Route>

The parent uses to render child routes.

Programmatic Navigation

const navigate = useNavigate();
navigate("/login");        // push
navigate(-1);              // go back
navigate("/home", { replace: true }); // replace history

On this page

Detailed Theory

Why Routing Exists

A traditional website asks the server for a brand-new HTML page on every link click. A single-page app loads once and then swaps content in place β€” much faster, no full-page flicker. But the URL still has to change so the back button, refresh, and shareable links all work.

That's what a router does: keep the URL in sync with what's on screen, *without* a real navigation.

> The library you'll most often see is React Router. Next.js has its own built-in router (covered separately).

Setting Up React Router

npm install react-router-dom

import { BrowserRouter, Routes, Route, Link } from "react-router-dom";

function App() { return ( <BrowserRouter> <nav> <Link to="/">Home</Link> <Link to="/about">About</Link> </nav>

<Routes> <Route path="/" element={<Home />} /> <Route path="/about" element={<About />} /> <Route path="*" element={<NotFound />} /> </Routes> </BrowserRouter> ); }

Three pieces:

Dynamic Route Parameters

Put :name in the path to capture part of the URL:

<Route path="/users/:id" element={<UserProfile />} />

import { useParams } from "react-router-dom"; function UserProfile() { const { id } = useParams(); return <p>User ID: {id}</p>; }

/users/42 β†’ id === "42".

Programmatic Navigation

When you can't use a (e.g., after form submit), use useNavigate:

import { useNavigate } from "react-router-dom";

function LoginForm() { const navigate = useNavigate(); async function onSubmit(e) { e.preventDefault(); await login(...); navigate("/dashboard"); } return <form onSubmit={onSubmit}>…</form>; }

navigate(-1) is "back". navigate("/x", { replace: true }) replaces the current entry instead of pushing a new one (useful after login).

Nested Routes & Layouts

Routes can nest to share layout (a navbar, a sidebar):

<Routes>
  <Route element={<DashboardLayout />}>
    <Route path="dashboard"        element={<Overview />} />
    <Route path="dashboard/users"  element={<Users />} />
    <Route path="dashboard/orders" element={<Orders />} />
  </Route>
</Routes>

Inside DashboardLayout you render an where the matched child should appear:

import { Outlet } from "react-router-dom";

function DashboardLayout() { return ( <div className="grid grid-cols-[200px_1fr]"> <Sidebar /> <main><Outlet /></main> </div> ); }

is a that knows when *its* route is the current one:

<NavLink
  to="/about"
  className={({ isActive }) => isActive ? "text-blue-500" : ""}
>
  About
</NavLink>

URL Search Params

Anything after ? is search params (also called query string). Use useSearchParams:

import { useSearchParams } from "react-router-dom";

function Products() { const [params, setParams] = useSearchParams(); const page = Number(params.get("page") ?? 1);

return ( <button onClick={() => setParams({ page: page + 1 })}> Next </button> ); }

URL params are great for filters, pagination, and sorting β€” they're shareable and the back button just works.

Beginner Mistakes to Skip

1. Using for internal links β€” full page reload, state lost. 2. Forgetting to render in a layout β€” child routes silently vanish. 3. Mismatched param names β€” then useParams().id returns undefined. 4. Putting outside β€” runtime error. 5. Hard-coded URLs everywhere β€” keep paths in a constants file or generate with generatePath.

Intermediate: How Client-Side Routing Actually Works

When you click a :

1. The router calls history.pushState() β€” the URL bar updates, but no server request happens. 2. The router compares the new URL against your route definitions. 3. The matched element renders. React reconciles the difference. 4. The page never reloads β€” only the parts that changed are updated.

This is why you must use (which talks to the router) instead of (which the browser handles).

Intermediate: Route-Match Ranking

In React Router v6, routes are ranked, not first-match:

/users/new        β†’  beats  /users/:id
/users/:id/posts  β†’  beats  /users/:id
/users/:id        β†’  beats  /users
  • β†’ always last
Order in the file doesn't matter; specificity does.

Intermediate: Protected (Auth-Guarded) Routes

A small wrapper redirects unauthenticated users:

import { Navigate, useLocation } from "react-router-dom";

function ProtectedRoute({ children }) { const { user } = useAuth(); const location = useLocation(); if (!user) { return <Navigate to="/login" replace state={{ from: location }} />; } return children; }

<Route element={<ProtectedRoute><DashboardLayout /></ProtectedRoute>}> <Route path="dashboard" element={<Overview />} /> </Route>

The state={{ from: location }} trick lets the login page redirect back where the user wanted to go.

Intermediate: Data Loaders (v6.4+)

The data router lets you fetch *before* the route renders, eliminating the "spinner inside a spinner" problem:

import { createBrowserRouter, RouterProvider, useLoaderData } from "react-router-dom";

const router = createBrowserRouter([ { path: "/users/:id", loader: ({ params }) => fetch(/api/users/${params.id}).then(r => r.json()), element: <UserProfile />, errorElement: <ErrorPage />, }, ]);

function UserProfile() { const user = useLoaderData(); return <h1>{user.name}</h1>; }

Loaders kick off in parallel for nested routes, so you don't waterfall.

Intermediate: Form Actions

Routes can also declare action functions for form submissions β€” they run on submit, return a result, then the loaders re-run automatically:

{
  path: "/posts/new",
  action: async ({ request }) => {
    const data = await request.formData();
    await createPost(data);
    return redirect("/posts");
  },
  element: <NewPostForm />,
}

This pattern (loaders + actions) is straight from server-side frameworks like Remix and is React Router's modern direction.

Advanced: Lazy Routes (Code Splitting)

Don't ship the entire app on first load β€” split per route:

const Settings = lazy(() => import("./Settings"));

<Route path="settings" element={ <Suspense fallback={<Spinner />}> <Settings /> </Suspense> } />

Or, with the data router:

{ path: "settings", lazy: () => import("./Settings") }

Advanced: Scroll Restoration

By default, the browser keeps the scroll position from the previous page. Usually you want to scroll to top on route change:

import { ScrollRestoration } from "react-router-dom";
<ScrollRestoration />

(Or do it manually in a small useEffect watching useLocation().)

Advanced: Hash vs Browser vs Memory Routers

RouterURL looks likeUse case
BrowserRouter/aboutDefault; needs server fallback to index.html
HashRouter/#/aboutStatic hosting where you can't configure the server
MemoryRouter(none)Tests, native apps

If you deploy to a plain static host and refreshing /about 404s, that's the server fallback issue β€” either configure it or switch to HashRouter.

Advanced: useLocation & Transitions

useLocation() gives you the current URL info, including pathname, search, and state (anything you passed via navigate(..., { state })).

Pair with useTransition (React 18) to keep the UI snappy while loading the next route's data:

const [pending, startTransition] = useTransition();
startTransition(() => navigate("/heavy-page"));

Practice Path

1. Build a 3-page site with /, /about, /users/:id and a -based navbar with active styling. 2. Add a nested /dashboard layout with sidebar + child routes overview, users, orders. 3. Build a search page that stores its query in URL search params (so refreshing keeps results). 4. Add a ProtectedRoute wrapper that bounces unauthenticated users to /login and back.