Auto-Updating Offline-Capable SPAs in React

Published on 07 March 2018

If you're reading this, you probably don't need to be sold on the 'why' of ensuring your users have the most up-to-date version of your PWA (progressive web app). However, there are a couple considerations when it comes time to implement auto-updates.

Should you have offline support (popularly implemented with the offline-plugin), your app will be cached by the browser. Unless your users do hard reloads whenever they access the app, and don't have other instances open, they may be stuck with an older version for longer than you'd like.

The offline-plugin uses ServiceWorkers to handle the update process. This is documented in the plugin, but the code required has been included here for completeness:

Step 1: Enable ServiceWorker Events

Enable events for ServiceWorkers where you instantiate the OfflinePlugin.

new OfflinePlugin({
  ServiceWorker: {
    events: true
  }
})

Step 2: Handle Updates Using Life-Cycle Events

The two events that really matter here are onUpdateReady and onUpdated. onUpdateReady can remain as is, but you can modify onUpdated to dispatch some action that will allow you to display a message to the user that there is an update available, and give them a link to reload at their leisure. The original snippet provided in the documentation simply reloads the page when it's ready, but if the user is in the middle of something that would be somewhat inconvenient.

If you need to display other progress notices, you can use onUpdating and onUpdateFailed as well.

const runtime = require('offline-plugin/runtime');

runtime.install({
  onUpdating: () => {
    console.log('SW Event:', 'onUpdating');
  },
  onUpdateReady: () => {
    console.log('SW Event:', 'onUpdateReady');
    // Tells to new SW to take control immediately
    runtime.applyUpdate();
  },
  onUpdated: () => {
    console.log('SW Event:', 'onUpdated');
    // Reload the webpage to load into the new version
    // window.location.reload();
    // OR: Set flag to display update notice to user
  },
  onUpdateFailed: () => {
    console.log('SW Event:', 'onUpdateFailed');
  }
});

Step 3: Trigger Update Checks

You can trigger an update check using the following:

if ('serviceWorker' in navigator) {
  navigator.serviceWorker.register('/sw.js').then((registration) => {
    registration.update();
  });
}

Put this in a convenience function somewhere, and invoking it will kick off the life-cycle events described in the previous step.

Note: Your ServiceWorker file will need to be in your public root.

In regular React applications, with multiple routes, you would unobtrusively trigger an update check on route change. But in an SPA, depending on your implementation, this mechanism may not be applicable.

You have two options available to you here, check for updates at intervals, and/or invoke your update check function when the user performs specific actions such as at the end of a workflow, or another frequently used interaction.

Step 4: Prevent Caching of ServiceWorker and index.html

The final piece of the puzzle is to ensure that your application index.html and your ServiceWorker file (sw.js from the example in the previous step) are not cached.

On nginx it might look like the following, you should be able to find equivalents for Apache.

location /index.html {
       add\_header Cache-Control no-cache;
       expires 0;
       try\_files /index.html =404;
}

location /sw.js {
       add\_header Cache-Control "no-cache";
       proxy\_cache\_bypass $http\_pragma;
       proxy\_cache\_revalidate on;
       expires off;
}

And with that done, your SPA should now gracefully handle updates.