跪拜 Guibai
← All articles
Frontend

How React Router Turns a URL Change into a DOM Update Without a Page Refresh

By 小月土星 ·
Read original on juejin.cn ↗ Google Translate ↗ Alt translation

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.

Summary

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.

Takeaways
HashRouter listens for `hashchange` events, extracts the path after `#`, and pushes it into React state via Context—no server config needed.
`<Routes>` matches the first `<Route>` whose path fits the current URL, with static segments beating dynamic `:id` segments and `*` acting as a last-resort catch-all.
Child `<Route>` elements inside a parent `<Route>` use relative paths; the parent renders an `<Outlet />` where matched child content appears.
`<Navigate replace to="...">` swaps the current history entry instead of pushing a new one, preventing back-button traps on redirects.
`React.lazy(() => import('./Page'))` tells Vite to split that page into a separate chunk; `<Suspense fallback={...}>` shows a loading state until the chunk arrives.
`<Link>` renders a real `<a>` tag but calls `event.preventDefault()` and updates the hash internally, so the page never reloads.
`useParams()` reads the dynamic segments matched by the nearest `<Route>`, making `/user/:id` available as `{ id: "123" }` inside the rendered component.
The project’s 404 page uses `window.location.href = '/'` after 3 seconds, which triggers a full page reload—a `useNavigate` call would keep it SPA-native.
Conclusions

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.

Concepts & terms
HashRouter
A React Router container that synchronizes the UI with the URL hash fragment (`#/path`). It listens for the browser's `hashchange` event, extracts the path after `#`, and provides it to the component tree via React Context. Because the hash is never sent to the server, it requires zero server-side configuration.
BrowserRouter
A React Router container that uses the HTML5 History API (`pushState`, `replaceState`, and the `popstate` event) to manage clean URLs without a `#`. It requires server-side fallback configuration (e.g., Nginx `try_files`) to avoid 404 errors on direct access or refresh.
Outlet
A placeholder component in React Router v6+ that renders the matched child route inside a parent layout component. When a nested `<Route>` matches, its `element` is rendered at the `<Outlet />` position, enabling shared layouts across sub-pages.
React.lazy()
A React function that enables code-splitting by accepting a dynamic `import()` call. It returns a component that suspends rendering until the imported module loads, requiring a `<Suspense>` boundary to show a fallback UI during loading.
useParams
A React Router hook that returns an object of key-value pairs for the dynamic segments (e.g., `:id`) matched by the current `<Route>`. In nested routes, it reads from the nearest matching `<Route>` context.
Source: juejin.cn ↗ Google Translate ↗ Backup ↗