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
Registering a Service Worker
If you navigate to src/index.js
you find on the last line:
serviceWorker.unregister();
Switch it to:
serviceWorker.register();
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 }),
});
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,
}
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
);
And we can show an Alert when it is true
:
{isServiceWorkerInitialized && (
<Alert text="Page has been saved for offline use" />
)}
Alert 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}
/>
)}
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();
}
});
}
};
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 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.
Top comments (12)
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:
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?
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 :)
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):
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!
thanks Gert, I think your code should be included the CRA boilerplate
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?
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.
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!
Hey buddy! Thank you so much for your comment here. It worked for me like magic :)
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
Hi. I would need to see the code to help. Can you create a public repo or use codesandbox.io/?