How React Router Turns a URL Change into a DOM Update Without a Page Refresh
Understanding the hashchange-to-Context-to-render pipeline removes the magic from React Router and makes debugging route mismatches, 404s on refresh, and back-button loops straightforward. The same mental model transfers directly to BrowserRouter and any client-side router built on the History API.
A Vite 8 + React 19 + react-router-dom v7.18 project walks through the full front-end routing stack, from the hashchange event that makes HashRouter tick to the `<Outlet />` placeholder that enables nested layouts. The codebase implements eight core mechanisms: hash-based routing, a declarative `<Routes>/<Route>` table, dynamic `:id` parameters with `useParams`, parent-child nested routes, `<Navigate>` redirects with history replacement, a `path="*"` 404 fallback, route-level code splitting via `React.lazy()` and `<Suspense>`, and `<Link>` components that intercept clicks to prevent full-page navigation.
The piece traces a single route switch end-to-end—clicking a `<Link>` updates `location.hash`, fires `hashchange`, triggers a React state update inside HashRouter, and causes `<Routes>` to re-match and render the new component tree, all without a server round-trip. It also contrasts HashRouter with BrowserRouter, explains why redirects should use `replace` to avoid back-button traps, and shows how dynamic `import()` splits a 500KB bundle into per-route chunks that load on demand.
The piece treats the `hashchange` event as the atomic unit of client-side routing, which demystifies every abstraction layer above it and makes the HashRouter-to-BrowserRouter migration a one-line import swap.
Explaining `<Routes>` matching as a priority system—static over dynamic, depth over shallowness—clarifies why `/products/new` renders the `new` child route and not the `:productId` catch-all, a common point of confusion.
Calling out that the 404 page uses a full-page reload via `window.location.href` instead of `useNavigate` highlights a subtle SPA purity trade-off that many tutorials ignore.
The `replace` prop on `<Navigate>` is framed as a back-button correctness issue, not just a stylistic choice, which is the right lens for production routing.