Create React App: Service Worker
When doing a recent project I decided to add offline capabilities. This was a dictionary app that relied on internet connectivity, and each new request was expensive time-wise. Luckily, Create React App comes with an already configured Service Worker that eases turning a React project into a Progressive Web App.
Enabling the Service Worker
Inside src/index.js
change serviceWorker.unregister()
to serviceWorker.register()
.
This will allow service worker to cache external resources and app files. Which for many use cases is enough.
Note that by default service workers will update itself only on total page restart, which means closing all tabs on a device and opening the app again. This can be confusing.
Updating the Service Worker
Sometimes updating the app is critical to its functionality. On a mobile device a hidden open tab can prevent a service worker update. Which can lead to a lot of frustration for both the developer and user.
The easiest way around this is to trigger a force update inside a service worker. This can be potentially dangerous if the user enters any data inside the app, so keep that in mind.
Currently, there's no way to add a config option inside webpack to enable this.
Let's do it manually, using fs
module from node.
After running npm build
a build/service-worker.js
will be created:
importScripts(
"https://storage.googleapis.com/workbox-cdn/releases/3.6.3/workbox-sw.js"
);
importScripts("/precache-manifest.1627229ec6fca0a0029e621a9027a2fd.js");
workbox.clientsClaim();
self.__precacheManifest = [].concat(self.__precacheManifest || []);
workbox.precaching.suppressWarnings();
workbox.precaching.precacheAndRoute(self.__precacheManifest, {});
workbox.routing.registerNavigationRoute("/index.html", {
blacklist: [/^\/_/, /\/[^\/]+\.[^\/]+$/],
});
To enable force an update self.skipWaiting()
needs to be added here.
And node is well suited for this job.
- Create a file
modifyServiceWorker.js
const fs = require("fs");
fs.readFile("build/service-worker.js", "utf8", (err, data) => {
if (err) return console.error(err);
const snippet = `
self.addEventListener('install', event => {
self.skipWaiting();
});
`;
const result = data.replace(
"workbox.clientsClaim();",
`workbox.clientsClaim();\n${snippet}`
);
fs.writeFile("build/service-worker.js", result, "utf8", (readError) => {
if (readError) return console.log(readError);
});
});
- Modify the npm build script
{
"scripts": {
"build": "react-scripts build && node modifyServiceWorker.js"
}
}
This will skip the waiting
lifecycle of service worker and update it immediately, which
can be good and bad. This depends on the app itself.
Prompting the user to update
For better user experience would be to ask the user if they are ready for an update.
This is slightly more complicated, but in the end more considerate.
To achieve such behavior we need to display a button, that will send a message to the service worker on clicking it. Here is a general idea:
- Adjust
modifyServiceWorker.js
to listen for amessage
const fs = require("fs");
fs.readFile("build/service-worker.js", "utf8", (err, data) => {
if (err) return console.error(err);
const snippet = `
addEventListener('message', messageEvent => {
if (messageEvent.data === 'skipWaiting') return skipWaiting();
});
`;
const result = data.replace(
"workbox.clientsClaim();",
`workbox.clientsClaim();\n${snippet}`
);
fs.writeFile("build/service-worker.js", result, "utf8", (readError) => {
if (readError) return console.log(readError);
});
});
- Modify
serviceWorker.js
and trigger anonUpdate
hooks forregistration.waiting
function registerValidSW(swUrl, config) {
navigator.serviceWorker
.register(swUrl)
.then(registration => {
if (registration.waiting) {
// Prompt user to update service workers
if (config && config.onUpdate) {
config.onUpdate(registration)
}
}
registration.onupdatefound = () => {
// ...
installing.onstatechange = () => {
// ...
}
}
})
}
- Add a
div
insidepublic/index.html
for rendering messages
<div id="worker-message"></div>
- Use the
onUpdate
hooks insideindex.js
:
serviceWorker.register({
onUpdate: (registration) => {
if (registration.waiting) {
ReactDOM.render(
<ServiceWorkerMessage registration={registration} />,
document.querySelector("#worker-message")
);
}
},
});
- Create
ServiceWorkerMessage
that will work in two steps by sending a message and listening for a change event
import React, { useState, useEffect } from "react";
export const ServiceWorkerMessage = ({ registration }) => {
const [show, setShow] = useState(true);
useEffect(() => {
navigator.serviceWorker.addEventListener("controllerchange", () => {
window.location.reload();
});
}, []);
return (
<React.Fragment>
{show && (
<div className="message" role="alert">
<p className="message__text">
Your app is ready for an update. Please save any data before
proceeding.
</p>
<button
className="message__button"
onClick={() => {
setShow(false);
registration.waiting.postMessage("skipWaiting");
}}
>
Update
</button>
</div>
)}
</React.Fragment>
);
};
On clicking the Update
button all open tabs inside the browser will be refreshed.
You can view the GitHub Repo for a working example.
Recap
There are many ways to update a service worker. We've seen three:
- Background update, that works when user manually closes all the tabs
- Force with a
skipWaiting
inside the service worker. - Prompt user for an action that refreshes all the open tabs.
For more information on service worker read the excellent documentation on developers.google.com.