Converting Multi-Page apps to SPA

If you want to build a SPA (single page application) from a MPA (multi page application) you don't always need a giant framework. What you need mainly is to set an event handler on your links to disable their default behaviour, then invoke the function that updates your page, and lastly push a new history to browser history to update the URL without refreshing the page. Below is example of a typescript function that does this:

function enableSpaMode() {
    document.querySelector('main')?.addEventListener('click', (e) => {
        const link = e.target as HTMLAnchorElement;
        if (link.nodeName === 'A') {
            e.preventDefault();
            const href = link.getAttribute('href')!;
    
            // Do nothing when link to current page is clicked
            if (link.search === window.location.search) {
                return;
            }
    
            // update state using queries in link
            const params = new URLSearchParams(link.search);
            if (params.size > 0) {
                for (const [key, value] of params) {
                    state[key] = value;
                }
            }
    
            // invoke function that updates your page
            render(state);
    
            // update browser history and url
            window.history.pushState(state, '', href);
        }
    });
    
    // add support for browser back button
    window.addEventListener("popstate", function(e) {
        render(e.state);
    });
}

Since when user lands on your app for the first time, browser automatically stores the history without a state, we need to replace that history after app has updated the state and rendered the page by calling history's replaceState method:

window.history.replaceState(state, '', window.location.search);

There is one little problem however and that is if someone uses browser back button to navigate back to a previous page browser won't show the page in exact scrolling position that it was when user left that page. To solve this problem we need to update the state in browser history by storing scroll position there before we make render a new page. We can achieve that using this:

// replace existing history state with one that has scrolling position
const scrollPosition = document.documentElement.scrollTop;
const tempState = {...history.state, scroll: scrollPosition};
history.replaceState(tempState, '', window.location.search);

Then when popState event fires we get the scroll position from state and scroll to that:

window.addEventListener("popstate", async (event) => {
    await renderPage(event.state);
    if (event.state.scroll) {
        window.scrollTo(0, event.state.scroll);
    }
});

Full script:
https://github.com/smohadjer/pokerrangliste/blob/master/public/ts/index.ts

SPA demo:
https://tournaments.myendpoint.de/

GitHub repository