Make an AJAX call when the user leaves a web page (using onbeforeunload)

29/3/2017

Sometimes you need to send some data to the server before the user leaves your web application. A good example of this is saving data. Instead of giving the user a save button to save the changes he made, the application saves all data if the user navigates away from the page or closes the browser.

To implement this, the window.onbeforeunload browser event is used which is supported by all major browsers. It fires when the user leaves the current page. It doesn't matter if he's clicking a link and navigates away, if he quits the browser or if he enters a new address in the address bar of his browser. Attaching a handler to the event goes like this:

window.onbeforeunload = function(){
    $.ajax({
        type: 'POST',
        contentType: 'application/json; charset=utf-8',
        url: '/save-data',
        data: { },
        success: function(result) { }
      });
}

I'm using jQuery to make it all a little easier, $.ajax is a helper function to make an asynchronous call to the server. It would be nice if the article ended here and everything just worked. Unfortunately this is still implemented in Javascript and it runs inside a browser. In this case, Safari is ruining the party. Sometimes the call succeeds, sometimes nothing has happened. Safari doesn't wait until the AJAX call is completed to redirect the user or close the browser. To fix this we can execute the AJAX call synchronously. The browser will wait until the call is finished and then do the appropriate action. By setting the async property on the $.ajax parameter you can make the call synchronously.

window.onbeforeunload = function(){
    $.ajax({
        type: 'POST',
        contentType: 'application/json; charset=utf-8',
        url: '/save-data',
        data: { },
        success: function(result) { },
        async: true // <-- make the call synchronous 
      });
}

Be aware that you only do this for ajax calls that are executed in the onbeforeunload handler. If you do this for all your AJAX calls, your application will freeze whenever a call to the server is made. Another thing to consider: don't do this with calls that take long to complete. The user will have to wait until the call finishes, it will be a frustrating experience.

The data is now being saved consistently over all browsers, that problem is tackled. Unfortunately Safari has another problem, caused by one of its features. It caches the page aggressively but does not take AJAX calls into account. In some cases, when the user presses the back button, old data is shown on the page. If the user would then leave the page the old data would be saved by the handler of the onbeforeunload event. The fix for this is quite ugly, but it does work. To determine if a page is being shown we handle the onpageshow event. The name is quite descriptive, it fires when a page is shown in the browser.

window.onpageshow = function(event) {
    if (event.persisted) {
        window.isRefresh = true;
        window.location.reload();
    }
};

The event.persisted property indicates if the page was cached by the browser. If the page was cached, a global isRefresh flag is set to true and the page is reloaded to discard the cached page and load the correct data. By reloading the page the onbeforeunload handler which is defined will be executed. This can't happen or the old, cached data would be saved by the ajax call. The onbeforeunload handler is changed to take the isRefresh property into account to determine whether the data should be saved.

window.onbeforeunload = function(){
    if(!window.isRefresh) {
        $.ajax({
            type: 'POST',
            contentType: 'application/json; charset=utf-8',
            url: '/save-data',
            data: { },
            success: function(result) { },
            async: true
          });
    }

    window.isRefresh = false;
}

Setting the window.isRefresh flag to false is actually not necessary, because the browser will reload, close or navigate to another page when the ajax call finished. But better safe than sorry I guess.

There you have it, as always nothing is easy when it needs to run in the browser, but it works. 'Till next time!