Browser history functioning and loopback gotcha
30 Jan 2017According to MDN,
The
History
interface allows to manipulate the browser session history, that is the pages visited in the tab or frame that the current page is loaded in.
But how exactly does the browser keep track of the visited pages?
Without dwelling into technicalities, it is safe to assume that a doubly linked list is being used under the hood. A doubly linked list (abbreviated as DLL) has the following benefits over a singly linked list or a simple array:
- Dynamically add and remove elements (beats arrays)
- Bi-directional movement is easy (beats singly linked list)
Push
When we visits a new url, a new entry is added to the DLL.
An application can use history.pushState()
to simulate url based navigation.
element.onclick = function handleClick(e) {
history.pushState(
state, // tiny session storage state
title,
url // destination URL
);
};
We can travel along the DLL and re-visit the pages.
history
object has 3 methods for travelling:
// move relative to the current entry
history.go(1);
history.go(2);
history.go(10);
history.go(-10);
history.go(0);
history.back(); // equivalent to history.go(-1)
history.forward(); // equivalent to history.go(1)
Pop
But these traversal methods must be invoked using JS (example: a button click).
If we use the browser’s navigation buttons, we can still intercept the action by listening for the popstate
event.
window.onpopstate = e => {
console.log(document.location, e.state);
};
Branching
Apart from moving back-and-forth, we can also branch out of the current list. If we are on some intermediate entry and navigate to a new URL (not back/forward movement, but redirection), then the subsequent entries will be lost. And a new branch will emerge, with the previous entries.
Demo
Note: Use the BACK
and FORWARD
buttons provided in the demo and not the browser’s buttons.
The Gotcha
When working with the history API, it is possible to mess things up - perhaps by not connecting the browser’s navigation tools to the app, or by create loopback loops.
Loop… what?
Picture this: We make a web app, possibly a PWA, and there’s a bug in the routing logic.
Visiting the page /foo
redirects us to some other path, /bar
.
If we now try to go back to /foo
… BAM!. /foo
redirects us back to /bar
.
Or how about this: Interacting with the page /foo
, somehow, redirects us to path /bar
.
But we don’t want to be on /bar
, and there’s a cancel
or no thanks
button in the app.
We press the button, and instead of going back to /foo
, we are redirected to /foo
.
If we now go back
… BAMMMM!. We leave the new /foo
, go back to /bar
, again deny to use the services provided by /bar
and are redirected to - you guessed it - a new copy of /foo
.
Still with me? Sounds confusing? How ‘bout a demo, eh?
In the demo below, try cancelling the login prompt and then going back. You will find it IMPOSSIBLE.
This abnormality is located in lines 56-59 and can easily be fixed.
Instead of using history.pushState()
, use history.back()
.
The solution is used here.
function loginRedirect() {
- var backUrl = location.hash.match(/ret=(.*)/)[1];
- history.pushState({}, '', backUrl);
+ history.back();
}
The End
And that’s the end of it. The problem demonstrated here exists in the production build of an actual product online - organisation kept anonymous for obvious reasons.
If you are interested in the implementations, here’s the code: