Preventing back button navigation in SPAs

For sites using a SPA (Single Page Application) architecture on the client-side, it’s difficult to come up with an alternative to window.onbeforeunload event which is provided by the browser. When the client is merely switching different views and not actual pages, there is no window.onbeforeunload event to tap into.

So what do you do if you want to prevent users from navigating away from a certain view using the back-button? I’m going to talk about strategy which will allow us to mimick behavior offered by window.onbeforeunload. It basically relies on adding a random hash to the URL and then tapping into the window.onhashchange event when that hash is removed.

Here’s how it works:

TL;DR: Scroll to the bottom for a link to the demo

First when you load a view from which you want to prevent your users from navigating away with the back-button, call a function to add a random hash to the URL:

self.addRandomHash = function() {
  // This will harmlessly change the url hash to "#random",
  // which will trigger onhashchange when they hit the back button
  if (_.isEmpty(location.hash)) {
    var random_hash = '#ng-' + new Date().getTime().toString(36);

    // Push "#random" onto the history, making it the most recent "page"
    history.pushState({navGuard: true}, '', random_hash)
  }
};

You’ll notice that we prefixed our random hash with #ng- and added a navGuard attribute to the history, this is done mainly so we can detect navigation events related to our nav guard logic and prevent things like tracking page events in Google Analytics or initializing 3rd party modules, etc.

So now you want to subscribe to the hash change event. When it changes, show a native confirm dialog similar to what you see with the window.onbeforeunload event:

$(window).off('hashchange.ng').on('hashchange.ng', function(event) {
  if (_.isEmpty(location.hash)) {
    var msg = 'Are you sure you want to navigate away from this screen? You may lose unsaved changes.';
    var result = confirm(msg);
    if (result) {
      //Go back to where they were trying to go
      //Only go back if there is something to go back to
      if (window.history.length > 2) {
        window.history.back();
      }
    } else {
      // Put the hash back in; rinse and repeat
      window.history.forward();
    }
  }
});

//While we are at it, also throw in the traditional beforeunload listener to guard against accidantal window closures
$(window).off('beforeunload.ng').on('beforeunload.ng', function(event) {
  return msg;
});

So what you see there is, if they confirm the prompt, we take them back to the previous page if there is one. window.history.length should be greater than 2 because we have to count the current URL without the hash and the previous URL with the hash.

Also another thing you want to make sure is to not show that prompt when they are navigating within the app using JavaScript i.e switching views. So disable the listener if they just clicked on anything other than the browser’s back/forward button:

$('a').not('a,a:not([href]),[href^="#"],[href^="javascript"]').mousedown(function() {
  $(window).off('beforeunload.ng');
});    

That’s it. It’s a bit of a hack but that’s the best I could could think of. I’d love to hear of any other techniques that you guys are using.

Demo: https://jsfiddle.net/qz2p9b63/3/ (To test, hit your browser’s back button)

If you have any questions or comments, please post them below. If you liked this post, you can share it with your followers or follow me on Twitter!