Hash vs. History Routing: What Actually Happens in the Browser
Choosing between hash and history routing determines whether you need server-side fallback configuration and whether your URLs will be crawlable. Getting this wrong means 404s on refresh or ugly URLs that break shareability and analytics.
A single-page application switches views by intercepting navigation and swapping DOM content, but without routing, the URL never changes. Front-end routing solves this by maintaining a mapping table between paths and components, updating the address bar, and rendering the correct view — all without a full page load.
Hash routing relies on the fragment identifier after `#`. Changes to the hash don't trigger a navigation request, and the `hashchange` event makes it trivial to wire up. The implementation is dead simple and works everywhere, but the `#` in the URL looks clunky and hurts SEO.
History routing uses the `pushState` and `replaceState` methods to change the URL path cleanly, with `popstate` handling back/forward button presses. The result is a normal-looking URL, but it introduces a server-side dependency: a direct visit or refresh on any path will 404 unless the server is configured to fall back to the entry HTML file.
Hash routing is often dismissed as legacy, but its zero-config deployment makes it the safer default for static sites and embedded widgets where you can't control the server.
The 404 problem in history mode is not a browser bug — it's a consequence of the server treating every path as a real resource request, which SPAs fundamentally break.
Most frameworks abstract these two modes behind a single API, but understanding the underlying events explains why `popstate` fires on back/forward but not on `pushState` — a distinction that trips up custom router implementations.
The mapping-table pattern — an array of path/component pairs — is the same regardless of mode, which means the routing strategy is a pluggable concern, not a structural one.