Frontend Development 11 min read

Web Routing in Single‑Page Applications: Hash, History, and Memory Modes

The article explains client‑side routing for single‑page applications, detailing three approaches—hash mode using location.hash and onhashchange, history mode leveraging the HTML5 History API’s pushState/replaceState with popstate handling, and memory mode storing routes in JavaScript or localStorage—plus their trade‑offs regarding URL readability, SEO, server configuration, and browser support.

Tencent Music Tech Team
Tencent Music Tech Team
Tencent Music Tech Team
Web Routing in Single‑Page Applications: Hash, History, and Memory Modes

In the previous article we introduced Single‑Page Applications (SPA) and the problem that a page refresh reloads the single HTML file, causing the current view to be lost. Because the browser does not request a new page when navigating, a client‑side router is required.

The router must satisfy two objectives: (1) load different modules according to the page URL without triggering a full page request, and (2) listen to URL changes. The two classic implementations are the hash mode and the history mode.

1. Hash mode

The URL hash (the part after “#”) is a readable and writable string that does not affect the server. Changing the hash does not cause an HTTP request, but each change adds a new entry to the browser’s history stack, enabling back/forward navigation.

Core APIs:

window.location.hash – read/write the hash value. window.onhashchange (or addEventListener('hashchange', …) ) – fires when the hash changes (supported by IE8+, Firefox 3.6+, Chrome 5+, Safari 4+).

Simple implementation of a hash router:

class HashRouter {
    constructor(routeArr = []) {
        // store callbacks
        this.routers = {};
        routeArr.forEach(item => this.register(item));
    }
    register(item) {
        const { name, content } = item;
        this.routers[name] = typeof content === 'function' ? content : function() {};
    }
    load() {
        const hash = location.hash.slice(1);
        const handler = this.routers[hash];
        try { handler.apply(this); } catch (e) { console.error(e); }
    }
}

window.addEventListener('hashchange', router.load.bind(router), false);

After registering routes and adding the hashchange listener, clicking a link such as #index updates the view without a page reload.

2. History mode

The HTML5 History API allows manipulation of the session history stack without reloading the page. It provides three new methods:

history.pushState(stateObj, title, url) – adds a new entry. history.replaceState(stateObj, title, url) – replaces the current entry. history.state – returns the current state object.

Because pushState/replaceState change the URL without a reload, the history mode satisfies the first router goal. However, these calls do **not** trigger the popstate event; the event fires only when the user navigates via the browser’s back/forward buttons or when history.back() / history.forward() are called.

Simple implementation of a history router:

class HistoryRouter {
    constructor(routeArr = []) {
        this.routers = {};
        routeArr.forEach(item => this.register(item));
        this.listenPopState();
    }
    register(item) {
        const { path, content } = item;
        this.routers[path] = typeof content === 'function' ? content : function() {};
    }
    listenPopState() {
        window.addEventListener('popstate', e => {
            const state = e.state || {};
            const path = state.path || '';
            this.load(path);
        }, false);
    }
    push(path) {
        history.pushState({ path }, null, path);
        this.load(path);
    }
    replace(path) {
        history.replaceState({ path }, null, path);
        this.load(path);
    }
    load(path) {
        const handler = this.routers[path];
        try { handler.apply(this); } catch (e) { console.error(e); }
    }
}

In addition, the router can intercept clicks on <a> elements, prevent the default navigation, and call push() or replace() accordingly.

3. Memory mode

Memory mode keeps routing information entirely in JavaScript memory (or localStorage ) while the URL stays unchanged. It is useful for environments where the URL cannot be modified, such as React‑Native.

const routes = {
  '/index': '这是首页',
  '/about': '这是关于页',
  '/detail': '这是详情页'
};
const container = document.getElementById('container');
function route() {
  let href = window.localStorage.getItem('cur-route') || '/index';
  container.innerHTML = routes[href];
}
const allA = document.querySelectorAll('a.link');
allA.forEach(a => {
  a.addEventListener('click', e => {
    e.preventDefault();
    const href = a.getAttribute('href');
    window.localStorage.setItem('cur-route', href);
    route();
  });
});
route();

After initializing the router, clicking a link updates the stored route and re‑renders the corresponding content.

Comparison

Hash mode works in all browsers, requires no server configuration, but produces unfriendly URLs and is not SEO‑friendly.

History mode yields clean URLs and better SEO, but needs server‑side support (fallback to the SPA entry) and is unsupported in IE8 and below.

Memory mode keeps the URL unchanged, so browser navigation buttons do not work; it is suitable for native‑like or offline scenarios.

When using history mode, the server must be configured to return the SPA’s entry page for any unknown path to avoid 404 errors on page refresh.

Frontend DevelopmentSPAhash modehistory APIweb routing
Tencent Music Tech Team
Written by

Tencent Music Tech Team

Public account of Tencent Music's development team, focusing on technology sharing and communication.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.