94

I am using History API for my web app and have one issue. I do Ajax calls to update some results on the page and use history.pushState() in order to update the browser's location bar without page reload. Then, of course, I use window.popstate in order to restore previous state when back-button is clicked.

The problem is well-known — Chrome and Firefox treat that popstate event differently. While Firefox doesn't fire it up on the first load, Chrome does. I would like to have Firefox-style and not fire the event up on load since it just updates the results with exactly the same ones on load. Is there a workaround except using History.js? The reason I don't feel like using it is — it needs way too many JS libraries by itself and, since I need it to be implemented in a CMS with already too much JS, I would like to minimize JS I am putting in it.

So, would like to know whether there is a way to make Chrome not fire up popstate on load or, maybe, somebody tried to use History.js as all libraries mashed up together into one file.

Brian Tompsett - 汤莱恩
  • 5,753
  • 72
  • 57
  • 129
spliter
  • 12,321
  • 4
  • 33
  • 36
  • 3
    To add, Firefox's behavior is the specced and correct behavior. As of Chrome 34, it's now fixed! Yay for the Opera devs! – Henry May 03 '14 at 06:37

15 Answers15

49

In Google Chrome in version 19 the solution from @spliter stopped working. As @johnnymire pointed out, history.state in Chrome 19 exists, but it's null.

My workaround is to add window.history.state !== null into checking if state exists in window.history:

var popped = ('state' in window.history && window.history.state !== null), initialURL = location.href;

I tested it in all major browsers and in Chrome versions 19 and 18. It looks like it works.

Pavel Linkesch
  • 3,546
  • 3
  • 22
  • 23
  • 5
    Thanks for the heads up; I think you can simplify the `in` and `null` check with just `!= null` since that will take care of `undefined` as well. – pimvdb May 18 '12 at 11:03
  • 5
    Using this will only work if you always use `null` in your `history.pushState()` methods like `history.pushState(null,null,'http//example.com/)`. Granted, that'd probably most usages (that's how it's set up in jquery.pjax.js and most other demos). But if the browsers implements `window.history.state` (like FF and Chrome 19+), [window.history.state could be non-null on a refresh or after a browser restart](https://developer.mozilla.org/en/DOM/Manipulating_the_browser_history#Reading_the_current_state) – unexplainedBacn Jun 08 '12 at 04:36
  • 19
    This isn't working anymore for me in Chrome 21. I wound up just setting popped to false on page load and then setting popped to true on any push or pops. This neuters Chrome's pop on first load. It's explained a bit better here: https://github.com/defunkt/jquery-pjax/issues/143#issuecomment-6194330 – Chad von Nau Sep 02 '12 at 11:20
  • I came with this same problem and end using the simple @ChadvonNau solution. – Pherrymason Jun 17 '13 at 20:27
  • In the same link @ChadvonNau posted, Josh answers and says it's enough to use the condition: (event.state && event.state.container). That worked for me – Fractalf Oct 08 '13 at 08:28
  • Thanks Chad von Nau for the solution! It took me a day to solve this issue. But my biggest issue was to realize the AJAX and regular URLs couldn't be exactly the same, which Chrome doesn't seem to handle well. (http://stackoverflow.com/questions/6309477/moving-back-to-a-pushstate-entry-that-used-ajax/6836213#6836213) – Flavio Wuensche Nov 07 '13 at 20:40
  • ('state' in window.history && window.history.state !== null) returns different in Firefox & IE10. Now IE10 does not pop on reload either, so this solution faults – primavera133 Jan 15 '14 at 15:04
  • Another approach that we used was to pass a state with each call to pushState/replaceState and then detect on (e.originalEvent.state) inside the popstate function. On initial page load the passed state is null – Georgio_1999 Jul 17 '14 at 08:55
  • Thanks @unexplainedBacn making first two parameter as null worked for me – kaustubh badamikar Oct 17 '14 at 17:51
30

In case you do not want to take special measures for each handler you add to onpopstate, my solution might be interesting for you. A big plus of this solution is also that onpopstate events can be handled before the page loading has been finished.

Just run this code once before you add any onpopstate handlers and everything should work as expected (aka like in Mozilla ^^).

(function() {
    // There's nothing to do for older browsers ;)
    if (!window.addEventListener)
        return;
    var blockPopstateEvent = document.readyState!="complete";
    window.addEventListener("load", function() {
        // The timeout ensures that popstate-events will be unblocked right
        // after the load event occured, but not in the same event-loop cycle.
        setTimeout(function(){ blockPopstateEvent = false; }, 0);
    }, false);
    window.addEventListener("popstate", function(evt) {
        if (blockPopstateEvent && document.readyState=="complete") {
            evt.preventDefault();
            evt.stopImmediatePropagation();
        }
    }, false);
})();

How it works:

Chrome, Safari and probably other webkit browsers fire the onpopstate event when the document has been loaded. This is not intended, so we block popstate events until the the first event loop cicle after document has been loaded. This is done by the preventDefault and stopImmediatePropagation calls (unlike stopPropagation stopImmediatePropagation stops all event handler calls instantly).

However, since the document's readyState is already on "complete" when Chrome fires onpopstate erroneously, we allow opopstate events, which have been fired before document loading has been finished to allow onpopstate calls before the document has been loaded.

Update 2014-04-23: Fixed a bug where popstate events have been blocked if the script is executed after the page has been loaded.

Torben
  • 6,317
  • 1
  • 33
  • 32
  • 4
    Best solution so far for me. I've been using the setTimeout method for years, but it causes buggy behavior when page is loading slowly / when user triggers pushstate very quickly. `window.history.state` fails on reload if a state was set. Setting variable before pushState clutters code. Your solution is elegant and can be dropped on top of other scripts, without impacting the rest of codebase. Thanks. – kik Mar 24 '14 at 16:23
  • 4
    **This is THE solution!** If only I read this far down days ago when trying to solve this problem. This is the only one that has worked flawlessly. Thank you! – Ben Apr 10 '14 at 07:59
  • 1
    Excellent explanation and the only solution worth a try. – Sunny R Gupta May 29 '14 at 11:50
  • This is a great solution. At time of writing, latest chrome & FF fine, issue is with safari (mac & iphone). This solution works like a charm. – Vin Jan 19 '15 at 17:12
  • Pure gold! Proper handling and not relying on timers! – Mikael Gidmark Apr 14 '15 at 10:59
  • 1
    Woot! If Safari is ruining your day, these are the codes you are looking for! – DanO Apr 22 '15 at 17:42
  • Worked for me on Chrome :) – Zhong Ri Oct 08 '19 at 18:19
28

Using setTimeout only isn't a correct solution because you have no idea how long it will take for the content to be loaded so it's possible the popstate event is emitted after the timeout.

Here is my solution: https://gist.github.com/3551566

/*
* Necessary hack because WebKit fires a popstate event on document load
* https://code.google.com/p/chromium/issues/detail?id=63040
* https://bugs.webkit.org/process_bug.cgi
*/
window.addEventListener('load', function() {
  setTimeout(function() {
    window.addEventListener('popstate', function() {
      ...
    });
  }, 0);
});
Sonny Piers
  • 365
  • 3
  • 4
25

The solution has been found in jquery.pjax.js lines 195-225:

// Used to detect initial (useless) popstate.
// If history.state exists, assume browser isn't going to fire initial popstate.
var popped = ('state' in window.history), initialURL = location.href


// popstate handler takes care of the back and forward buttons
//
// You probably shouldn't use pjax on pages with other pushState
// stuff yet.
$(window).bind('popstate', function(event){
  // Ignore inital popstate that some browsers fire on page load
  var initialPop = !popped && location.href == initialURL
  popped = true
  if ( initialPop ) return

  var state = event.state

  if ( state && state.pjax ) {
    var container = state.pjax
    if ( $(container+'').length )
      $.pjax({
        url: state.url || location.href,
        fragment: state.fragment,
        container: container,
        push: false,
        timeout: state.timeout
      })
    else
      window.location = location.href
  }
})
spliter
  • 12,321
  • 4
  • 33
  • 36
  • 8
    This solution stopped working for me on Windows (Chrome 19), still working on Mac (Chrome 18). Seems like history.state exists in Chrome 19 but not 18. – johnnymire Apr 16 '12 at 13:10
  • 1
    @johnnymire I posted my solution for Chrome 19 lower: http://stackoverflow.com/a/10651028/291500 – Pavel Linkesch May 18 '12 at 10:42
  • Somebody should edit this answer to reflect this post-Chrome 19 behavior. – Jorge Suárez de Lis Sep 03 '13 at 07:45
  • 5
    For anyone looking for a solution, the answer from Torben works like a charm on current Chrome and Firefox versions as of today – Jorge Suárez de Lis Oct 21 '13 at 11:56
  • 1
    Now in April 2015, I used this solution to solve an issue with Safari. I had to use the onpopstate event in order to achieve a handling pressing the back button on a hybrid iOS/Android app using Cordova. Safari fires its onpopstate even after a reload, which I called programmatically. With the way my code was setup, it went into an infinite loop after reload() was called. This fixed it. I only needed the first 3 lines after the event declaration (plus the assignments at the top). – Martavis P. Apr 02 '15 at 04:44
14

A more direct solution than reimplementing pjax is set a variable on pushState, and check for the variable on popState, so the initial popState doesn't inconsistently fire on load (not a jquery-specific solution, just using it for events):

$(window).bind('popstate', function (ev){
  if (!window.history.ready && !ev.originalEvent.state)
    return; // workaround for popstate on load
});

// ... later ...

function doNavigation(nextPageId) {
  window.history.ready = true;

  history.pushState(state, null, 'content.php?id='+ nextPageId); 
  // ajax in content instead of loading server-side
}
Ilia Ross
  • 13,086
  • 11
  • 53
  • 88
Tom McKenzie
  • 742
  • 8
  • 19
  • Wouldn't that if always evaluate to false? I mean, `window.history.ready` is always true. – Andriy Drozdyuk Jan 30 '12 at 18:55
  • Updated the example to be a little more clear. Basically, you just want to only handle popstate events once you give the all-clear. You could achieve the same effect with a setTimeout 500ms after load, but to be honest it's a bit cheap. – Tom McKenzie Feb 01 '12 at 12:20
  • 3
    This should be the answer IMO, I tried Pavels solution and it did not work properly in Firefox – Porco Jun 26 '12 at 18:06
  • This worked for me as an easy solution to this annoying problem. Thanks a lot! – nirazul Apr 11 '13 at 10:15
  • `!window.history.ready && !ev.originalEvent.state` -- those two things are *never* defined when I refresh the page. so therefore no code executes. – chovy Sep 27 '13 at 04:56
  • `window.history.ready` is never defined during page load, only after you handle a link click and before you call `pushState`. You should also be loading the appropriate content as part of your `ready` handler *if required*. In this example, `progress.php` would be loaded with a url param (server-side). Assume we'll ajax in the url param otherwise. – Tom McKenzie Oct 21 '13 at 05:48
4

Webkit's initial onpopstate event has no state assigned, so you can use this to check for the unwanted behaviour:

window.onpopstate = function(e){
    if(e.state)
        //do something
};

A comprehensive solution, allowing for navigation back to the original page, would build on this idea:

<body onload="init()">
    <a href="page1" onclick="doClick(this); return false;">page 1</a>
    <a href="page2" onclick="doClick(this); return false;">page 2</a>
    <div id="content"></div>
</body>

<script>
function init(){
   openURL(window.location.href);
}
function doClick(e){
    if(window.history.pushState)
        openURL(e.getAttribute('href'), true);
    else
        window.open(e.getAttribute('href'), '_self');
}
function openURL(href, push){
    document.getElementById('content').innerHTML = href + ': ' + (push ? 'user' : 'browser'); 
    if(window.history.pushState){
        if(push)
            window.history.pushState({href: href}, 'your page title', href);
        else
            window.history.replaceState({href: href}, 'your page title', href);
    }
}
window.onpopstate = function(e){
    if(e.state)
        openURL(e.state.href);
};
</script>

While this could still fire twice (with some nifty navigation), it can be handled simply with a check against the previous href.

som
  • 2,023
  • 30
  • 37
  • 1
    Thanks. The pjax solution stopped working on iOS 5.0 because the `window.history.state` is also null in iOS. Just checking if the e.state property is null is enough. – Husky Sep 19 '12 at 14:51
  • Are there any known problems with this solution? It works perfectly on any browsers I test. – Jacob Ewing Feb 12 '14 at 19:14
3

This is my workaround.

window.setTimeout(function() {
  window.addEventListener('popstate', function() {
    // ...
  });
}, 1000);
Baggz
  • 17,207
  • 4
  • 37
  • 25
1

Here's my solution:

var _firstload = true;
$(function(){
    window.onpopstate = function(event){
        var state = event.state;

        if(_firstload && !state){ 
            _firstload = false; 
        }
        else if(state){
            _firstload = false;
            // you should pass state.some_data to another function here
            alert('state was changed! back/forward button was pressed!');
        }
        else{
            _firstload = false;
            // you should inform some function that the original state returned
            alert('you returned back to the original state (the home state)');
        }
    }
})   
prograhammer
  • 20,132
  • 13
  • 91
  • 118
0

The best way to get Chrome to not fire popstate on a page load is to up-vote https://code.google.com/p/chromium/issues/detail?id=63040. They've known Chrome isn't in compliance with the HTML5 spec for two full years now and still haven't fixed it!

Dave
  • 1,212
  • 12
  • 8
0

In case of use event.state !== null returning back in history to first loaded page won't work in non mobile browsers. I use sessionStorage to mark when ajax navigation really starts.

history.pushState(url, null, url);
sessionStorage.ajNavStarted = true;

window.addEventListener('popstate', function(e) {
    if (sessionStorage.ajNavStarted) {
        location.href = (e.state === null) ? location.href : e.state;
    }
}, false);
  • window.onload = function() { if (history.state === null) { history.replaceState(location.pathname + location.search + location.hash, null, location.pathname + location.search + location.hash); } }; window.addEventListener('popstate', function(e) { if (e.state !== null) { location.href = e.state; } }, false); – ilusionist Apr 16 '16 at 15:47
-1

The presented solutions have a problem on page reload. The following seems to work better, but I have only tested Firefox and Chrome. It uses the actuality, that there seems to be a difference between e.event.state and window.history.state.

window.addEvent('popstate', function(e) {
    if(e.event.state) {
        window.location.reload(); // Event code
    }
});
likeitlikeit
  • 5,563
  • 5
  • 42
  • 56
-1

I know you asked against it, but you should really just use History.js as it clears up a million browser incompatibilities. I went the manual fix route only to later find there were more and more problems that you'll only find out way down the road. It really isn't that hard nowadays:

<script src="//cdnjs.cloudflare.com/ajax/libs/history.js/1.8/native.history.min.js" type="text/javascript"></script>

And read the api at https://github.com/browserstate/history.js

ubershmekel
  • 11,864
  • 10
  • 72
  • 89
-1

This solved the problem for me. All I did was set a timeout function which delays the execution of the function long enough to miss the popstate event that is fired on pageload

if (history && history.pushState) {
  setTimeout(function(){
    $(window).bind("popstate", function() {
      $.getScript(location.href);
    });
  },3000);
}
Jeremy Lynch
  • 6,780
  • 3
  • 52
  • 63
-2

This worked for me in Firefox and Chrome

window.onpopstate = function(event) { //back button click
    console.log("onpopstate");
    if (event.state) {
        window.location.reload();
    }
};
Alexey
  • 7,127
  • 9
  • 57
  • 94
-2

You can create an event and fire it after your onload handler.

var evt = document.createEvent("PopStateEvent");
evt.initPopStateEvent("popstate", false, false, { .. state object  ..});
window.dispatchEvent(evt);

Note, this is slightly broke in Chrome/Safari, but I have submitted the patch in to WebKit and it should be available soon, but it is the "most correct" way.

Kinlan
  • 16,315
  • 5
  • 56
  • 88