How the Humble URL Hash Fragment Became the Foundation of Single-Page Apps
Hash routing is still the fastest path to client-side navigation when you can't touch the server—static sites, Electron apps, and embedded webviews all rely on it. Understanding the `hashchange` event and the routing table pattern demystifies what React Router and Vue Router actually do under the hood.
Traditional multi-page applications reload the full HTML document on every link click, causing a white flash and re-rendering identical headers, nav bars, and footers. The core insight behind single-page apps is that the URL's hash fragment—originally designed for in-page anchor jumps—changes without triggering an HTTP request. By listening for the `hashchange` event and mapping hash values to DOM replacement callbacks, a developer can build client-side routing with zero server configuration.
A hand-written `HashRouter` class in vanilla JavaScript distills this pattern to three essential pieces: a routing table that maps hash strings to render functions, a `hashchange` listener that fires on every navigation, and a single mount point where content gets swapped. The same architecture underpins every major framework router, from React Router's `<HashRouter>` to Vue Router's `createWebHashHistory`.
The trade-off is real: hash URLs look ugly, break SEO, and hide route state from the server. Those limitations directly motivated the History API and server-side rendering strategies that came later, but the hash-based approach remains the simplest way to add routing to a static front end.
The jump from anchor links to application routing is a case study in repurposing a browser primitive far beyond its original intent—hash fragments were meant for document navigation, not application state.
Frameworks like React Router add a lot of ceremony, but the underlying mechanism is so simple it fits in a dozen lines of vanilla JS; the real complexity comes from nested routes, guards, and lazy loading, not the core pattern.
The `this` binding problem in `addEventListener` is a small but telling detail: it's exactly the kind of JavaScript quirk that framework abstractions hide, and exactly the kind that trips up developers who skip the fundamentals.