Engineering Blog

PWA update notifications in a React app

PWA update notifications in a React app

PWA update notifications in a React app

Discover how we built the system behind PWA update notifications in a React app at Toplyne.

Discover how we built the system behind PWA update notifications in a React app at Toplyne.

Discover how we built the system behind PWA update notifications in a React app at Toplyne.

Table Of Contents

Never Miss An Update

Ideally, you would always want users to use the latest version of your application. Having an interactive experience that speaks to the user when you push a new feature or make a new release or fix a bug through visual feedback: there is a new update… please refresh to view the fresh changes: is what our requirements were at ツ Toplyne. I’ll walk you through my experience of building such a system.

What does the end goal look like?

The requirements are:

  • Check if a new update is available for the app, show a notification saying “A new version of the app is available, REFRESH?”

What does the end goal look like?
  • When the user navigates, check if there is a new version and manually update the page (this is a rather opinionated approach which works perfectly as per our use case, we do not surprise users with a reload, we show the above notification first, if users do not refresh themselves, we do it for them on a subsequent page navigation). This is pretty easy to do once the previous requirement is in place.

We did leverage the PWA (Progressive Web App) feature that comes with a React app. Using this, we get to use the superpowers of a service worker. Some notable features of service workers are:

  • runs in the background independent of main js thread

  • can be used to cache static assets and network requests

  • can be paired with workbox to make your app run in offline mode

In our case, we utilized a service worker to check for updates periodically and made use of the service worker’s update event to show notification and/or update the app. You can read more about the service worker life cycle here. Learning about the lifecycle is crucial in understanding the what, when, and why, and how to override the default behavior.

No Service Worker

image credits: hasura.io

Enough talk, show me the code already

We had disabled the default service worker that comes with CRA, so we had to add it manually. Adding this boilerplate is quite straight forward. Use the cra-template-pwa and copy whatever you need. Generally, it’s the workbox dependencies in package.json, service-worker.js, serviceWorkerRegistration.js.


These files contain the basic logic for registering a service worker in your browser. Ideally, you’d import the serviceWorker from serviceWorkerRegistration.js and call the register method on it in a suitable location/component. By default, it is index.js, you can move it to wherever you like but the component should always render.

For my use case, I moved it to App.tsx to make use of React state and effects which would provide helpers to solve our use cases.

Checking for updates and showing a notification

This is our first task. We need to periodically check for updates and if there is an update found, show the notification to refresh the page.

The workflow is:

  1. poll periodically to check for sw (short for service worker) updates by calling sw’s update() function. Update the default service worker registeration in serviceWorkerRegistration.js file. This happens in a separate thread and is non blocking to the main js thread, so calling setInterval() should be okay.


  1. When we call the update() method, it will compare new and older versions and if they are even a little bit different, it installs the new worker on the browser. When a new worker is found, the default behavior is to not replace the old worker straight away, this is done for obvious reasons so that user interaction doesn't get ruined. The new worker goes into a forever waiting phase until the old worker is invalidated (usually 24 hours/ hard refresh/ closing tabs and opening).

Checking for updates and showing a notification
  1. We need to manually tell it to skip_waiting and take control… We do this by adding a custom event listener to the service worker to listen for custom messages. Add the following at the end of service-worker.js file.


This step is very important because it allows us to hook custom handlers to control the lifecycle of a service worker according to our needs.

Putting everything in order, showing the update notification

I’ve written a custom hook 🚀 useServiceWorker.ts which initializes the service worker registration and exposes functions to control the visibility of our notification alert.


When a service worker update is found, we store it as a waitingWorker. This gives us the control over calling SKIP_WAITING manually whenever we need to (in our case it's on clicking of REFRESH button on the update notification).

How and where to show the notification

I’ve used this custom hook in my App entrypoint App.tsx. You can use it wherever you want to, but the component should always render and better be a parent to all your child components.


and that’s it…..

Considerations

There are a lot of things you need to take care of when you’re testing a service worker deployment. Deploying a buggy service worker can ruin your app experience and these things are very hard to get rid of since they exist on the client’s browser and take a long time to get invalidated. So consider the following points before deploying your new feature:

  • sw gets enabled on the production build of react, so it is useless to test it on dev. You wouldn’t want to test on dev as it leads to issues with hot reload and default caching mechanisms there.

  • service worker can cache your static assets, you don’t need to add cache rules for js, css, images in your deployment service manually.

  • (Very Important!!) Make sure not to cache your sw on your deployment service, else your new sw will never get installed, and you might never see a notification. Make sure your service-worker.js file has Cache-Control headers set to max-age=0,no-cache,no-store,must-revalidate. Read more about these headers here.

  • create-react-app includes a service worker by default and makes your app work offline by default.

  • (Very Important!) if you do run into issues with deployed service workers, push a release with calling unregister() to the sw and restore Cache-Control headers.

  • You sometimes may get an error in production that says “Failed to update a ServiceWorker for scope (‘{{hostname}}/’) with script (‘{{hostname}}/service-worker.js’): An unknown error occurred when fetching the script.”. This happens when you push a release and at the moment when your app is building, your sw checks for an update. This is a harmless thing and your app works fine on the next update check. (We’re currently figuring out what you can do to gracefully handle this error).

A lot of apps use service workers nowadays and it acts as a very useful tool if used correctly. With great power comes great responsibility. Peace out! ✌️

Working at Toplyne

We’re always looking for talented engineers to join our team. You can find and apply for relevant roles here.

Ideally, you would always want users to use the latest version of your application. Having an interactive experience that speaks to the user when you push a new feature or make a new release or fix a bug through visual feedback: there is a new update… please refresh to view the fresh changes: is what our requirements were at ツ Toplyne. I’ll walk you through my experience of building such a system.

What does the end goal look like?

The requirements are:

  • Check if a new update is available for the app, show a notification saying “A new version of the app is available, REFRESH?”

What does the end goal look like?
  • When the user navigates, check if there is a new version and manually update the page (this is a rather opinionated approach which works perfectly as per our use case, we do not surprise users with a reload, we show the above notification first, if users do not refresh themselves, we do it for them on a subsequent page navigation). This is pretty easy to do once the previous requirement is in place.

We did leverage the PWA (Progressive Web App) feature that comes with a React app. Using this, we get to use the superpowers of a service worker. Some notable features of service workers are:

  • runs in the background independent of main js thread

  • can be used to cache static assets and network requests

  • can be paired with workbox to make your app run in offline mode

In our case, we utilized a service worker to check for updates periodically and made use of the service worker’s update event to show notification and/or update the app. You can read more about the service worker life cycle here. Learning about the lifecycle is crucial in understanding the what, when, and why, and how to override the default behavior.

No Service Worker

image credits: hasura.io

Enough talk, show me the code already

We had disabled the default service worker that comes with CRA, so we had to add it manually. Adding this boilerplate is quite straight forward. Use the cra-template-pwa and copy whatever you need. Generally, it’s the workbox dependencies in package.json, service-worker.js, serviceWorkerRegistration.js.


These files contain the basic logic for registering a service worker in your browser. Ideally, you’d import the serviceWorker from serviceWorkerRegistration.js and call the register method on it in a suitable location/component. By default, it is index.js, you can move it to wherever you like but the component should always render.

For my use case, I moved it to App.tsx to make use of React state and effects which would provide helpers to solve our use cases.

Checking for updates and showing a notification

This is our first task. We need to periodically check for updates and if there is an update found, show the notification to refresh the page.

The workflow is:

  1. poll periodically to check for sw (short for service worker) updates by calling sw’s update() function. Update the default service worker registeration in serviceWorkerRegistration.js file. This happens in a separate thread and is non blocking to the main js thread, so calling setInterval() should be okay.


  1. When we call the update() method, it will compare new and older versions and if they are even a little bit different, it installs the new worker on the browser. When a new worker is found, the default behavior is to not replace the old worker straight away, this is done for obvious reasons so that user interaction doesn't get ruined. The new worker goes into a forever waiting phase until the old worker is invalidated (usually 24 hours/ hard refresh/ closing tabs and opening).

Checking for updates and showing a notification
  1. We need to manually tell it to skip_waiting and take control… We do this by adding a custom event listener to the service worker to listen for custom messages. Add the following at the end of service-worker.js file.


This step is very important because it allows us to hook custom handlers to control the lifecycle of a service worker according to our needs.

Putting everything in order, showing the update notification

I’ve written a custom hook 🚀 useServiceWorker.ts which initializes the service worker registration and exposes functions to control the visibility of our notification alert.


When a service worker update is found, we store it as a waitingWorker. This gives us the control over calling SKIP_WAITING manually whenever we need to (in our case it's on clicking of REFRESH button on the update notification).

How and where to show the notification

I’ve used this custom hook in my App entrypoint App.tsx. You can use it wherever you want to, but the component should always render and better be a parent to all your child components.


and that’s it…..

Considerations

There are a lot of things you need to take care of when you’re testing a service worker deployment. Deploying a buggy service worker can ruin your app experience and these things are very hard to get rid of since they exist on the client’s browser and take a long time to get invalidated. So consider the following points before deploying your new feature:

  • sw gets enabled on the production build of react, so it is useless to test it on dev. You wouldn’t want to test on dev as it leads to issues with hot reload and default caching mechanisms there.

  • service worker can cache your static assets, you don’t need to add cache rules for js, css, images in your deployment service manually.

  • (Very Important!!) Make sure not to cache your sw on your deployment service, else your new sw will never get installed, and you might never see a notification. Make sure your service-worker.js file has Cache-Control headers set to max-age=0,no-cache,no-store,must-revalidate. Read more about these headers here.

  • create-react-app includes a service worker by default and makes your app work offline by default.

  • (Very Important!) if you do run into issues with deployed service workers, push a release with calling unregister() to the sw and restore Cache-Control headers.

  • You sometimes may get an error in production that says “Failed to update a ServiceWorker for scope (‘{{hostname}}/’) with script (‘{{hostname}}/service-worker.js’): An unknown error occurred when fetching the script.”. This happens when you push a release and at the moment when your app is building, your sw checks for an update. This is a harmless thing and your app works fine on the next update check. (We’re currently figuring out what you can do to gracefully handle this error).

A lot of apps use service workers nowadays and it acts as a very useful tool if used correctly. With great power comes great responsibility. Peace out! ✌️

Working at Toplyne

We’re always looking for talented engineers to join our team. You can find and apply for relevant roles here.