Front-End Routing in 20 Lines: How Hash Routing Works
Hash routing is still the default fallback when the History API isn't available, and understanding its 20-line core reveals the fundamental pattern behind every client-side router: intercept a URL change, look up a handler, and swap DOM. That pattern carries directly into React Router, Vue Router, and any framework's routing layer.
Before the History API, single-page applications faced a dilemma: changing the URL triggered a full page refresh, but keeping it static broke bookmarks and back-button navigation. Hash routing resolved this by exploiting the browser's existing anchor-link behavior. The fragment after `#` never gets sent to the server, yet modifying it updates the address bar and creates a history entry. A `hashchange` event listener detects these changes and swaps DOM content accordingly. The entire mechanism fits into a HashRouter class of about 20 lines: a key-value routing table maps hash paths to render callbacks, and `location.hash.slice(1)` strips the leading `#` for clean lookups. The approach is a textbook case of repurposing existing infrastructure — anchor links meant for in-page scrolling became the foundation of client-side navigation. Its obvious downsides are the permanent `#` in URLs, zero SEO value since the fragment never reaches the server, and poor compatibility with server-side rendering. HTML5's History API later solved these by allowing full path changes without reloads, but the core principle — intercept navigation and replace content with JavaScript — remains identical.
The hash routing pattern is a clean example of repurposing existing browser behavior rather than waiting for a purpose-built API. Anchor links were designed for in-page scrolling, but their side effects — no server request, history entry creation — turned out to be exactly what client-side routing needed.
The entire concept scales down to a key-value lookup triggered by an event. That simplicity explains why hash routing appeared in countless early SPA frameworks and why it remains a viable fallback when `pushState` isn't available.