I have created a web app which utilizes the history
pushState
and replaceState
methods in order to navigate through pages while also updating the history.
The script itself works almost perfectly; it will load pages correctly, and throw page errors when they need to be thrown. However, I've noticed a strange problem where pushState
will push multiple, duplicate entries (and replace entries before it) to the history.
For example, let's say I do the following (in order):
Load up index.php (history will be: Index)
Navigate to profile.php (history will be: Profile, Index)
Navigate to search.php (history will be: Search, Search, Index)
Navigate to dashboard.php
Then finally, this is what will come up in my history (in order of most recent to oldest):
Dashboard
Dashboard
Dashboard
Search
Index
The problem with this is that when a user clicks the forward or back buttons, they will either get redirected to the incorrect page, or have to click multiple times in order to go back once. That, and it'll make no sense if they go and check their history.
This is what I have so far:
var Traveller = function(){
this._initialised = false;
this._pageData = null;
this._pageRequest = null;
this._history = [];
this._currentPath = null;
this.abort = function(){
if(this._pageRequest){
this._pageRequest.abort();
}
};
// initialise traveller (call replaceState on load instead of pushState)
return this.init();
};
/*1*/Traveller.prototype.init = function(){
// get full pathname and request the relevant page to load up
this._initialLoadPath = (window.location.pathname + window.location.search);
this.send(this._initialLoadPath);
};
/*2*/Traveller.prototype.send = function(path){
this._currentPath = path.replace(/^\/+|\/+$/g, "");
// abort any running requests to prevent multiple
// pages from being loaded into the DOM
this.abort();
return this._pageRequest = _ajax({
url: path,
dataType: "json",
success: function(response){
// render the page to the dom using the json data returned
// (this part has been skipped in the render method as it
// doesn't involve manipulating the history object at all
window.Traveller.render(response);
}
});
};
/*3*/Traveller.prototype.render = function(data){
this._pageData = data;
this.updateHistory();
};
/*4*/Traveller.prototype.updateHistory = function(){
/* example _pageData would be:
{
"page": {
"title": "This is a title",
"styles": [ "stylea.css", "styleb.css" ],
"scripts": [ "scripta.js", "scriptb.js" ]
}
}
*/
var state = this._pageData;
if(!this._initialised){
window.history.replaceState(state, state.title, "/" + this._currentPath);
this._initialised = true;
} else {
window.history.pushState(state, state.title, "/" + this._currentPath);
}
document.title = state.title;
};
Traveller.prototype.redirect = function(href){
this.send(href);
};
// initialise traveller
window.Traveller = new Traveller();
document.addEventListener("click", function(event){
if(event.target.tagName === "a"){
var link = event.target;
if(link.target !== "_blank" && link.href !== "#"){
event.preventDefault();
// example link would be /profile.php
window.Traveller.redirect(link.href);
}
}
});
All help is appreciated,
Cheers.
The big difference is, that while pushState will create a new entry in the browser's history, replaceState will only replace the current state. As a side effect of this, using the replaceState method will change the URL in the address bar, without creating a new history entry.
pushState() method adds an entry to the browser's session history stack.
The history. pushState() method can be used to push a new entry into the browser's history—and as a result, update the displayed URL—without refreshing the page. It accepts three arguments: state , an object with some details about the URL or entry in the browser's history.
The History. replaceState() method modifies the current history entry, replacing it with the state object and URL passed in the method parameters. This method is particularly useful when you want to update the state object or URL of the current history entry in response to some user action.
Do you have a onpopstate handler ?
If yes, then check there also if you're not pushing to history. That some entries are removed/replaced in the history list might be a big sign. Indeed, see this SO answer:
Keep in mind that history.pushState() sets a new state as the newest history state. And window.onpopstate is called when navigating (backward/forward) between states that you have set.
So do not pushState when the window.onpopstate is called, as this will set the new state as the last state and then there is nothing to go forward to.
I once had exactly the same problem as you describe, but it was actually caused by me going back and forward to try to understand the bug, which would eventually trigger the popState handler. From that handler, it would then call history.push. So at the end, I also had some duplicated entries, and some missing, without any logical explanation.
I removed the call to history.push, replaced it by a history.replace after checking some conditions, and after it worked like a charm :)
EDIT-->TIP
If you can't locate which code is calling history.pushState:
Try by overwritting the history.pushState and replaceState functions with the following code:
window.pushStateOriginal = window.history.pushState.bind(window.history);
window.history.pushState = function () {
var args = Array.prototype.slice.call(arguments, 0);
let allowPush = true;
debugger;
if (allowPush ) {
window.pushStateOriginal(...args);
}
}
//the same for replaceState
window.replaceStateOriginal = window.history.replaceState.bind(window.history);
window.history.replaceState = function () {
var args = Array.prototype.slice.call(arguments, 0);
let allowReplace = true;
debugger;
if (allowReplace) {
window.replaceStateOriginal(...args);
}
}
Then each time the breakpoints are trigerred, have a look to the call stack.
In the console, if you want to prevent a pushState, just enter allowPush = false;
or allowReplace = false;
before resuming.
This way, you are not going to miss any history.pushState, and can go up and find the code that calls it :)
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With