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
<BrowserRouter>wraps your app, exposes the router context.<Routes>+<Route>declare the URL-to-component map.<Link>renders an anchor tag that callspushStateon click instead of reloading.useParams()reads dynamic URL segments inside a routed component.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).
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.