Fork me on GitHub

22 Feb 2011

JavaScript's history object, pushState and the back button

I’m not sure if it’s the immaturity of the browser support or my general uselessness but I’ve been having some trouble with the JavaScript history API.

I won’t try to explain the history API here, it’s pretty well covered at Mozilla Developer Network and W3. The basics are simple enough:

  • The API provides two methods; pushState which allows you to add a new entry to the browser history and replaceState which modifies the current history entry.
  • New entries added to history using pushState can be navigated via the browser’s back and forward buttons and a popstate event is fired on the window object when this happens.
  • Both methods allow you to attach arbitrary data to the history entry that you can use to reconstruct the appropriate page state when the user uses the back or forward buttons.

I imagine a pretty typical use-case is what I’ve been trying to do with the pagination and sorting on the list pages of Grails scaffolding. Instead of pagination links and column headers causing a full page reload when clicked I intercept the click event and send an AJAX request getting back a page fragment I can use to update the list in the page. Easy enough, however without the history API it will break the back and forward buttons and make deep linking impossible. This isn’t an acceptable option so in true progressive enhancement style I’ve used Modernizr and only apply the AJAX behaviour if the browser supports the history API.

The essence of the script involved is this:

var links =     //... pagination and sorting links
var container = //... the region of the page that will be updated with AJAX

// override clicks on the links'click', function() {
    // grab the link's URL
    var url = $(this).attr('href');
    // add a new history entry
    history.pushState({ path: url }, '', url);
    // load the page fragment into the container with AJAX
    // prevent the link click bubbling
    return false;

// handle the back and forward buttons
$(window).bind('popstate', function(event) {
    // if the event has our history data on it, load the page fragment with AJAX
    var state = event.originalEvent.state;
    if (state) {

// when the page first loads update the history entry with the URL
// needed to recreate the 'first' page with AJAX
history.replaceState({ path: window.location.href }, '');

At first glance this works pretty nicely. In browsers that support history (right now that’s just Chrome, Safari and its mobile variants) paginating and sorting the list does not refresh the entire page but the browser’s location bar is updated so copy-pasting or bookmarking the URL will give a valid link to the current page. What’s more, the back and forward buttons can be used to step back through the list pages just as if we reloaded the whole page. In non-history-compliant browsers the list page behaves just like it always did; the links reload the entire page.

Unfortunately there’s a problem that was reported to me on GitHub shortly after I uploaded a demo of my scaffolding work. Where everything falls down is when you paginate the list, follow a link off the page (or just use a bookmark or type in a URL), then use the back button to return to it. In Chrome and iPad/iPhone variants of Safari the browser displays just the page fragment from the last AJAX call, losing all the surrounding page along with styling, scripts, etc.

Where things get very odd is that adding a Cache-Control: no-cache header to the AJAX responses makes the problem disappear, presumably because the browser then doesn’t cache the AJAX response and has to use the historical URL to reload the entire page. Remember, in good progressive enhancement style the URL for the full page or the fragment is the same. The server uses the X-Requested-With header to decide whether to return a fragment or the whole page. Obviously, forgoing caching is hardly an acceptable compromise but it’s interesting in that it reveals what the browser is doing. It can’t, surely, be right for the browser to treat a page fragment the same as a full document!

Curiously in desktop Safari this doesn’t happen and the full page is loaded as you would hope. Looking at the User-Agent header it looks like Safari is using an older version of WebKit (533.19.4 rather than 534.13).

You can see the behaviour in my scaffolding sample app. I also ran into the exact same issue today at work whilst trying to add history navigation to this page (which is a good example of why you’d want to use history in the first place). I don’t think it’s just me, either. The same problem can be seen with the demo linked from this post about the history API.

If there are any JavaScript experts out there who can point out my obvious incompetence here, that would be great. Otherwise I guess I’ll have to wait for updates to WebKit to hopefully fix the issue before I can start applying this technique with confidence.


btilford said...

Sounds like a browser bug. Maybe history.js would be of some help or at least interesting.

Oliver Nightingale said...

I couldn't see the issue you mentioned in when trying your site in chrome, when going back after leaving the site I get taken back to the page with the sort order as it was.

Assuming that your server is also capable of rendering the table, for those browsers that don't support pushState, the server should probably handle those kind of requests, and once the page is loaded the JavaScript can take over.

I have a nice library that wraps pushState to provide this kind of routing, Davis.js it might be worth a look if you end up using this technique some more.

balupton said...

That bug and others are fixed in the new History.js by balupton.

Waleed said...

I encountered the same problem. Clicking a link to go to another page then hitting Back causes the browser to display the json response rather than the previous page.

History.js doesn't fix it. I switched to it hoping it would, but nothing changed. The Cache-control header fixed it, though. I had to use a value of "no-cache no-store" because just using "no-cache" didn't help. This is on Firefox 6.0.1.

Eric Sowell said...

I'm so glad you posted this. I thought I was just doing something stupid! And I agree with Waleed, history.js does not fix this. I'll try the caching bit.

stevenou said...

I have this exact same problem... Did you ever figure out a "real" solution? The cache-control fix is far from ideal, but I'll give it a shot. Thanks.

angel26 said...
This comment has been removed by a blog administrator.
Cruel said...

I use history.js and I had the same problem and afaik there is no solution until browsers themselves process pushState history differently. Like you, I don't like suppressing page caching, so I decided to render full pages via ajax and pull out the necessary information. Luckily my framework and site layout made this simple. Yes, it make the ajax request larger, but at least they're cached in their fullness. For now, this is the best solution for me.

Christopher J. Bottaro said...

I'm glad I found this post.

It looks like the Chrome devs don't want to fix it:

They say that a single given url shouldn't return two different things (html in one case and json in another).

That's strange though, because Rails (which I'm using) heavily encourages the use of the Accepts header to determine whether a given url should return html, json, js, xml, etc.

Add me to the list of people waiting for a real solution or the browsers makers to do something about it.

Martin Staflund said...

I found found that adding a query param to the url (e.g. "ajax=1"), when called via ajax, solved the problem. This way, the browser does not mix it up and page fragments loaded by Ajax still get cached.

Metamorfosis said...

Pelatihan SDM is a network marketing and training information or training an employee who has worked with many consulting firms and training institutions.
pendidikan dan pelatihan sdm, pelatihan akuntansi, sales marketing, manajemen bisnis, administrasi kantor, lembaga training motivasi, pelatihan sumber daya manusia, training manajemen, training sdm, informasi training, pusat pelatihan, pelatihan rumah sakit, training provider jakarta, training jakarta, training consultant jakarta, training center,

Kang Yudi said...

This post is very useful and informative, I really appreciate it.
pasang iklan gratis

Lan Linh said...

Thanks for the best blog. it was very useful for me.keep sharing such ideas in the future as well. Thanks for giving me the useful information. I think I need it!
car game
bike game
happy wheels demo
unblocked games
my little pony puzzle
my little pony halloween
free fun games

info bimtek said...

very useful and informative, I really appreciate it. good job awesome

pelatihan sdm

inhouse training

pelatihan gis

aplikasi perjalanan dinas

Trần Lucy said...

You are very good, I have followed your instructions and complete it
girl go game ,
cool math games ,
friv 4 ,
friv 2 ,
unblocked games happy wheels ,
juegos de autos de carrera ,
pou online ,
kizi 2 ,
juegos los simpson ,
kids games online ,
unblocked games happy wheels

Chang bui thi said...

Thanks for sharing this quality information with us. I really enjoyed reading.
friv4school , games2girls 2 , juegos de frozen gratis , kids games online , friv 2 , jogos do friv , , juegos de matar zombbies
, unblocked games , juegos de un show mas

Phoebe Vuong said...

Thank you so much for this article. I've been looking for it :) It will helps me much. I hope to see more and more posts of yours in the future :)
happy wheels | math games | starcraft html5 | starcraft 2 | starcraft html5 | starcraft | kid games | cool math games daily

Hieu Nguyen said...

thank very good post

Game Ark