DEV Community

loading...

Let Users Know When You Have Updated Your Service Workers in Create React App

glukmann profile image Gert Glükmann Originally published at Medium Updated on ・5 min read

Show an alert component when you have pushed a new service worker, allowing the user to update their page right away

Create React App (CRA) is great for developing progressive web apps (PWAs). It has offline/cache-first behaviour built in. It’s not enabled by default, but you can opt in. It uses service workers and has a lot of pitfalls you can read about from official docs.

This piece is going to show you how to trigger an alert (or toast or actually whatever component you want) when you have updated your service worker. Usually, this will be when your app has some new updates and you want the user to see them right away.

This piece assumes you have a new project made with CRA. If you don’t, you can do it easily with:

npx create-react-app my-app
Enter fullscreen mode Exit fullscreen mode

Registering a Service Worker

If you navigate to src/index.js you find on the last line:

serviceWorker.unregister();
Enter fullscreen mode Exit fullscreen mode

Switch it to:

serviceWorker.register();
Enter fullscreen mode Exit fullscreen mode

And registering a service worker is pretty much done. If you deploy your app to an HTTPS-enabled site, it’ll be cached.

Remember that service-worker implementation in CRA works only in production. You can make sure it works by checking the offline checkbox from the Chrome DevTools Network tab and reloading your page.

It still shows your app!

Is your updated Service Worker not visible?

Now comes the harder part. You add or change code in your app and deploy — but users aren’t seeing your updates. As the docs state:

The default behavior is to conservatively keep the updated service worker in the “waiting” state. This means that users will end up seeing older content until they close (reloading is not enough) their existing, open tabs.

What if you want users to see your new updates without having to close all tabs? CRA is providing that option, too.

In the src/serviceWorker.js is a function named registerValidSW that provides access to service-worker updates and success events via callbacks and also prints information about these events to the console. This is how you know when to show that the app is cached for offline use or there’s a newer version available.

The registerValidSW function takes in two arguments — the second one is the one we’re interested in. config can be an object that has onSuccess and onUpdate callbacks in it. You should wonder now how and where could we make such an object?

If you look where registerValidSW is called, you see that it comes from export function register(config). This is the very same function that we saw on the last line in src/index.js. Now, we’re back in our own code and we could do something like:

serviceWorker.register({
  onSuccess: () => store.dispatch({ type: SW_INIT }),
  onUpdate: reg => store.dispatch({ type: SW_UPDATE, payload: reg }),
});
Enter fullscreen mode Exit fullscreen mode

When those functions are called, they dispatch a function, and you can do whatever you want with them, like show a message.

With onSuccess it’s easier — you can just show the alert somewhere on your page. Perhaps it says, “Page has been saved for offline use.”. With onUpdate, you want to let the user know there’s a newer version available, and you can add a button with “Click to get the latest version.”.

Showing a user alert when page is first time saved for offline use

In the above example, I used Redux store to dispatch an action, and I have store set up with:

const initalState = {
  serviceWorkerInitialized: false,
  serviceWorkerUpdated: false,
  serviceWorkerRegistration: null,
}
Enter fullscreen mode Exit fullscreen mode

Now when dispatching SW_INIT type action, we change serviceWorkerInitialized state to true and can use this selector inside any React component.

In my src/App.js (or any other component), we get it from store with Redux Hooks:

const isServiceWorkerInitialized = useSelector(
  state => state.serviceWorkerInitialized
);
Enter fullscreen mode Exit fullscreen mode

And we can show an Alert when it is true:

{isServiceWorkerInitialized && (
  <Alert text="Page has been saved for offline use" />
)}
Enter fullscreen mode Exit fullscreen mode

Alert when Service Worker is installedAlert when Service Worker is installed

Showing the user an alert and a button when a new version of Service Worker is available

Using the same pattern, we show alert components when service workers have been updated.

{isServiceWorkerUpdated && (
  <Alert
    text="There is a new version available."
    buttonText="Update"
    onClick={updateServiceWorker}
  />
)}
Enter fullscreen mode Exit fullscreen mode

This time we add an onClick function that’ll be triggered when clicking on the “Update” button inside the alert component. Because we want user to click on a button and get a new version of the app.

All the magic is inside updateServiceWorker function that we are going to create.

This example is using CRA v3, which has a little addition generated inside the public/service-worker.js file. (If you’re using an older version of CRA, I’ve created a solution for that, too — just write to me.)

skipWaiting is a function that forces your new service worker to become the active one, and next time the user opens a browser and comes to your page, they can see the new version without having to do anything.

You can read more about skipWaiting from MDN. But this just forces your service worker to be the active one, and you see changes only next time. We want to ensure the user has a new version right now. That’s why we have to call it and then refresh the page ourselves — but only after the new service worker is active.

To call it, we need an instance of our new service worker. If you scroll back up to where we registered the service worker, you can see the onUpdate function had an argument called reg. That’s the registration object, and that’s our instance. This will be sent to the serviceWorkerRegistration property in the Redux store, and we can get our waiting SW from serviceWorkerRegistration.waiting.

This will be our function that’s called when the user presses the “Update” button inside the alert:

const updateServiceWorker = () => {
  const registrationWaiting = serviceWorkerRegistration.waiting;

  if (registrationWaiting) {
    registrationWaiting.postMessage({ type: 'SKIP_WAITING' });

    registrationWaiting.addEventListener('statechange', e => {
      if (e.target.state === 'activated') {
        window.location.reload();
      }
    });
  }
};
Enter fullscreen mode Exit fullscreen mode

Because the service worker is a worker and thus in another thread, to send any messages to another thread we have to use Worker.postMessage (MDN). Message type is 'SKIP_WAITING' as we saw from generated public/service-worker.js file.

And we create an eventListener that waits for our new service-worker state change, and when it’s activated, we reload the page ourselves. And that’s pretty much it.

Now the user can see there’s a newer version available, and if they want, they can update it right away.

Alert when new Service Worker is availableAlert when new Service Worker is available

Conclusion

I think it’s good to let the user decide if they want a new version right away or not. They have the option to click on the “Update” button and get the new version or just ignore it. Then, the new version of the app will be available when they close their tabs and open your app again.

Thanks.

Here’s a link to the example repository.

Discussion (12)

pic
Editor guide
Collapse
tomas223 profile image
Tomas Hornak • Edited

Great article. It helped me finally understand serviceWorkers.

After some more digging I started to wonder if it wouldn't be better to avoid storing ServiceWorkerRegistration and retrieve one when needed.
Should I concern that object can "expire" after some longer time?

I came to this:

if ('serviceWorker' in navigator) {
    navigator.serviceWorker.ready.then(registration => {
        const registrationWaiting = serviceWorkerRegistration?.waiting;
        ...
    });
}
Collapse
glukmann profile image
Gert Glükmann Author

Well yeah, when user doesn't click the Update button then maybe should think about the "expiring". Should test it out. Retriving it when needed probably would be safer solution?

Collapse
tomas223 profile image
Tomas Hornak

I will use your example for now and will see in future if there are any issues with it. If yes I will come back to report it :)

Collapse
ataraxis profile image
Florian Dollinger

Thanks! One problem here is that redux stores are not persistent. So if the user is pressing "refresh" on its browser instead of activating the waiting serviceworker, then the dialog wont pop up again. I solved that by

1) using redux-persist
2) not storing the the activated serviceworker inside the store, but getting it on the onClick() of the "update" button, just like this (very similar to Tomas Hornaks post, thank you too):

  const activateUpdate = () => {
    if ('serviceWorker' in navigator) {
      navigator.serviceWorker.ready.then(registration => {
        const serviceWorkerWaiting = registration.waiting;

        if (serviceWorkerWaiting) {
          console.log("inside waiting...")
          serviceWorkerWaiting.postMessage({ type: 'SKIP_WAITING' });
          serviceWorkerWaiting.addEventListener('statechange', e => {
            if (e.target.state === 'activated') {
              dispatch(swUpdateDone());
              window.location.reload();
            }
          });
        }
      });
    }
  }
Enter fullscreen mode Exit fullscreen mode
Collapse
sebastijandumancic profile image
Sebastijan Dumancic

Awesome stuff Gert, just implemented this and it works wonderfully. Potential bug is when user lands on the website with new SW waiting, but refreshes (or has open other tabs), after first showing of the notice to reload, it won't show up again and old service worker will still be visible.

But that's edge case, since that user will still get the new SW next time he lands on the website.

Either way, this really helped solve reloading. Thanks!

Collapse
wallaceturner profile image
wallaceturner

thanks Gert, I think your code should be included the CRA boilerplate

Collapse
manuelurbanotn profile image
manuelurbanotn

Hello Gert, nice post!
I'm working with a legacy app with and older version of CRA.
Can you share the workaround for older versions you mentioned before?

Collapse
glukmann profile image
Gert Glükmann Author

Hi!
You have create another file where you add the eventListener for message yourself (as shown in one of the gists) and then append that file to created service worker on build time. This can be done for example with github.com/tszarzynski/cra-append-sw.
Let me know if you have any troubles.

Collapse
manuelurbanotn profile image
manuelurbanotn

Hi Gert!
Finally I noticed that my app has Workbox integrated to Webpack, so I fixed it adding skipWaiting: true on my Webpack config.

Thanks for your time!

Thread Thread
niketsoni profile image
Niket Soni

Hey buddy! Thank you so much for your comment here. It worked for me like magic :)

Collapse
kingmauro profile image
Mauro

Hi Gert. Just what I was looking for. I just have one problem. It seems I cant pass the 'reg' argument from .register() to Redux so I can use it later on my Router. Any help would be great. Thanks

Collapse
glukmann profile image
Gert Glükmann Author

Hi. I would need to see the code to help. Can you create a public repo or use codesandbox.io/?