Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

pushState creating duplicate history entries and overwriting previous ones

Tags:

javascript

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):

  1. Load up index.php (history will be: Index)

  2. Navigate to profile.php (history will be: Profile, Index)

  3. Navigate to search.php (history will be: Search, Search, Index)

  4. 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.

like image 836
GROVER. Avatar asked Oct 30 '19 14:10

GROVER.


People also ask

What is the difference between pushState and replaceState?

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.

What does Window history pushState do?

pushState() method adds an entry to the browser's session history stack.

Does history pushState reload page?

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.

What is window history replaceState?

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.


1 Answers

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 :)

like image 100
Bonjour123 Avatar answered Oct 02 '22 15:10

Bonjour123