C.W.K.
Stream
Lesson 06 of 07 · published

SPA Routing with React Router

~18 min · react-router, routing, spa, history-api

Level 0React Novice
0 XP0/54 lessons0/12 achievements
0/100 XP to next level100 XP to go0% complete
A SPA is one HTML page. Without a router, navigating means full reloads. With one, the URL changes, the view swaps, and the browser back-button still does what users expect.

The problem a router solves

Your Vite app serves index.html at every URL. The browser loads main.tsx, mounts <App />, and stops. If a user clicks a link to /conversations/abc, the browser does a full reload — destroying React state — and the server sends the same index.html back. You're at the start every time.

A client-side router intercepts that click, updates the URL with history.pushState(), and tells React to render a different component tree based on the new URL. No reload. State survives. Back-button works.

React Router in five concepts

  1. <BrowserRouter> wraps your app, exposes the router context.
  2. <Routes> + <Route> declare the URL-to-component map.
  3. <Link> renders an anchor tag that calls pushState on click instead of reloading.
  4. useParams() reads dynamic URL segments inside a routed component.
  5. useNavigate() returns a function for programmatic navigation (e.g. after a form submit).

The minimal app

Routes can be flat or nested. Nested routes use the parent's component as a layout that renders <Outlet /> where the child fills in. That's how sidebars + main panels stay mounted across page changes (just like cwkPippa's chat UI).

NavLink vs Link

NavLink is a Link that knows whether its target matches the current URL. It exposes the active state via a function-as-className so you can highlight the current item in a nav bar without writing the matching logic yourself.

Server-side gotcha: the catch-all rewrite

When you deploy a SPA, any direct hit to a URL like https://yourapp.com/conversations/abc arrives at the server. The server sees no file at that path and 404s. Fix: configure the host to rewrite all non-asset paths to /index.html. Vercel, Cloudflare Pages, Netlify all have a one-line setting for this. Tauri apps don't need it (no server).

Some SPAs don't need routing at all. Cinder, the Cinder-style Tauri app you'll build later, is one window with one view. It binds to a backend conversation, shows a candidate board, and reacts to events. No URLs, no React Router. Don't add a router just because the tutorial says to — add it when you actually have multiple URL-addressable views.

Alternatives

TanStack Router is the rising challenger (type-safe routes, integrated search params). Wouter is the minimalist (1.5 KB, hook-based). React Router is the default. Pick TanStack if you want first-class TypeScript route inference; pick Wouter if you genuinely need to shave kilobytes. Otherwise React Router.

Code

main.tsx — wrap the app in a router·tsx
import React from "react";
import ReactDOM from "react-dom/client";
import { BrowserRouter } from "react-router-dom";
import App from "./App";
import "./index.css";

ReactDOM.createRoot(document.getElementById("root")!).render(
  <React.StrictMode>
    <BrowserRouter>
      <App />
    </BrowserRouter>
  </React.StrictMode>
);
App.tsx — flat + nested routes with a layout·tsx
import { Routes, Route, Outlet, NavLink, useParams } from "react-router-dom";

function Layout() {
  return (
    <div className="flex min-h-screen">
      <nav className="w-48 border-r p-4 space-y-2">
        <NavLink
          to="/"
          end
          className={({ isActive }) => isActive ? "text-brand" : "text-muted"}
        >
          Home
        </NavLink>
        <NavLink
          to="/conversations"
          className={({ isActive }) => isActive ? "text-brand" : "text-muted"}
        >
          Conversations
        </NavLink>
      </nav>
      <main className="flex-1 p-6">
        <Outlet />
      </main>
    </div>
  );
}

function Home() { return <h1>Welcome</h1>; }
function Conversations() { return <h1>List</h1>; }
function Conversation() {
  const { id } = useParams<{ id: string }>();
  return <h1>Conversation {id}</h1>;
}

export default function App() {
  return (
    <Routes>
      <Route element={<Layout />}>
        <Route path="/" element={<Home />} />
        <Route path="/conversations" element={<Conversations />} />
        <Route path="/conversations/:id" element={<Conversation />} />
        <Route path="*" element={<p>404</p>} />
      </Route>
    </Routes>
  );
}

External links

Exercise

Install react-router-dom in your bootstrap project. Wire up the layout above with three views (Home, Conversations, Conversation/:id). Confirm: (1) clicking NavLink updates the URL without reloading; (2) the back button works; (3) refreshing the page on /conversations/abc still shows the right view (requires either running locally where Vite handles it, or — if testing a build — the server rewrite mentioned in the lesson).
Hint
If the refresh-on-deep-URL test 404s, the dev server is fine (Vite handles it). For production, you'd need a _redirects file (Netlify), vercel.json rewrite, or equivalent. We cover this in Track 8.

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue

Comments 0

🔔 Reply notifications (sign in)
Sign inPlease sign in to comment.

No comments yet — be the first.