Learn how to customize Google Workbox’s default behavior for a service worker in your React app. This articles explains how to modify the behavior of the service worker life cycle, enabling fully automated updates of your app in the background. Includes a bonus section with a React Bootstrap component alert for app update announcements.
Setting Up A Service Worker
The smart way to get started with service workers is to use create-react-app, which provides a simple way to scaffold a basic service worker for a React app:
[code lang=”bash” classname=”codeboxes” gutter=”false”]npx create-react-app hello-world –template cra-template-pwa[/code]
This quick start approach gives your site basic offline capabilities by adding two boilerplate js modules to the root of your project, service-worker.js
and serviceWorkerRegistration.js
. It also installs Google’s npm package workbox-sw that vastly simplifies everything about managing a service worker.
By the way, a couple of reference sources that I came across bear mentioning. First, the web site create-react-app.dev contains documentation that explains how to customize these two boiler plate files to optimize your site’s offline behavior. Additionally, if you’re just getting started with service workers then I’d recommend this blog post from Danielly Costa, “Showing ‘new version available’ notification on create-react-app PWAs“. It’s well-written, easy to follow, it’s relatively current as of this publication, and it’s what I used when I was just getting started.
Moving along, the boiler plate code adds the following to your React app:
The only thing that I dislike is that this boiler plate does several things to bring attention to your site’s offline capabilities. I don’t want that. I want my site to be resilient with regard to Internet connectivity but I’d rather that my users don’t even notice when they’re offline. The less they notice, the better. Summarizing what you get versus what I actually wanted:
The default service worker life cycle
By default Workbox registers a service worker and then checks for and downloads any updates to your code base. At that point, it pauses the update process until all browser tabs and windows containing your app have been closed. Then, only upon the user’s next visit to your site does it actually update the locally cached content to activate these changes in the user’s browser. Additionally, just a slightly annoying side effect is that the boiler plate code sends messages to the javascript console any time the app is being accessed offline, and also when code is updated.
Versus what I wanted instead
By contrast, I not only want code updates downloaded and seamlessly activated but I also want the service worker to periodically check for updates say, at least once a day to ensure that users are never at risk of running dangerously outdated code.
I prototyped my desired behavior in my personal web site, lawrencemcdaniel.com (see the screen cast below). The source code is located in this Github repository in case you prefer to see the entire code base as opposed to the abbreviated code snippets that follow. I use the same code base for several other blog posts about ReactJS, Redux, REST api’s and front-end coding in general.
Some Editorial Comments On Hacking Google Workbox
I only had to modify a few files to achieve my desired behavior. The hard part, for me at least, was developing a technical understanding of what a service worker does, and then developing a commensurate understanding of how Google Workbox is trying to simplify how you work with them. With regard to the former, I found this documentation from Mozilla the most informative, “MDN Web Docs: Service Worker API“. Learning about Workbox on the other hand is more of a challenge. I ended up tracing the source code.
Service Workers are are powerful. The scaffolding that you get from create-react-app really only scratches the surface. Having said that, there are weird gaps in the Service Worker API. For example, there are events for ‘fetch’ and ‘install’ and ‘activate’ but not for ‘fetched’ and ‘installed’ and ‘activated’, even though there are defined states for these.
The service worker life cycle model is simple. A service worker has four possible states, which migrate as follows: installing, then installed, then activating, then activated. There’s also a general purpose exception state called redundant. The built-in wait period between ‘installed’ and ‘activating’ can be overriden by calling skipwaiting() (see an example of this in the default service-worker.js. You should note a couple of things however. First, it is recommended that your React app communicate with the service worker via a formal postMessage() api. And second, that regardless of how you call skipwaiting(), it will only have an effect if there is actually a installed worker in an ‘installed’ state.
Debugging service workers is a challenge. For one thing, they only run on your “production” build, which obviously complicates testing. But additionally, the very nature of service workers is that they directly control what version of your code you’re running, and that can get confusing. Lastly, its all event-driven which itself is challenging to trace and debug. My advise is to make copious use of console logging, as per the video screen cast above, so that execution threads are abundantly clear.
Summary of file modifications
Source File | Summary of changes |
---|---|
index.js | Remove boiler plate service worker registration |
service-worker.js | No modifications are necessary |
serviceWorkerRegistration.js | Add a hook for serviceWorkerRegistrationEnhancements |
serviceWorkerRegistrationEnhancements.js | New js module to implement onActivate event, plus, periodic update checks |
App.js | Refactor to class component. Add event handlers (callbacks) for Workbox service worker life cycle events. Add appUpdate components for announcements. |
appUpdate | React component to render Bootstrap announcements. |
index.js
Remove the reference to serviceWorkerRegistration. We’re going to migrate this to App.js in the next step so that we can leverage the App component life cycle for rendering announcements about changes to the service worker lifecycle.
[code lang=”js” classname=”codeboxes” gutter=”false”]
// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.
// Learn more about service workers: https://cra.link/PWA
serviceWorkerRegistration.register();[/code]
service-worker.js
I did not need to make any changes to this file as part of these modifications. Note however that I did make extensive modifications to service-worker.js when setting up the cache behavior of my React app, though this is out of scope of this blog post.
serviceWorkerRegistration.js
I added a hook for my own behavioral enhancements, as follows:
[code lang=”js” classname=”codeboxes” gutter=”false”] import { serviceWorkerRegistrationEnhancements } from "./serviceWorkerRegistrationEnhancements";
// approximately row 57
function registerValidSW(swUrl, config) {
navigator.serviceWorker
.register(swUrl)
.then((registration) => {
// —————————–
// my additional functionality
// —————————–
serviceWorkerRegistrationEnhancements(config, registration);
// —————————–
registration.onupdatefound = () => {
const installingWorker = registration.installing;
if (installingWorker == null) {
return;
}
installingWorker.onstatechange = () => {
if (installingWorker.state === ‘installed’) {
if (navigator.serviceWorker.controller) {
// At this point, the updated precached content has been fetched,
// but the previous service worker will still serve the older
// content until all client tabs are closed.
if (DEBUG) console.log(
‘serviceWorkerRegistration.js – New content is available and will be used when all ‘ +
‘tabs for this page are closed. See https://cra.link/PWA.’
);
// Execute callback
if (config && config.onUpdate) {
config.onUpdate(registration);
}
} else {
// At this point, everything has been precached.
// It’s the perfect time to display a
// "Content is cached for offline use." message.
if (DEBUG) console.log(‘serviceWorkerRegistration.js – Content is cached for offline use.’);
// Execute callback
if (config && config.onSuccess) {
config.onSuccess(registration);
}
}
}
};
};
})
.catch((error) => {
console.error(‘Error during service worker registration:’, error);
});
}
// more boiler plate code follows …..[/code]
serviceWorkerRegistrationEnhancements.js
This new additional js module implements an `onActivate` event which App.js listens for in order to raise a Bootstrap alert after updates are downloaded, installed, and activated. It also implements periodic daily checks for updates to the service worker.
[code lang=”js” classname=”codeboxes” gutter=”false”]
export function serviceWorkerRegistrationEnhancements(config, registration) {
const AUTOMATIC_UPDATE_CHECK_INTERVAL = 24; // expressed in hours
// OBJECTIVE 1.) SETUP A CALLBACK FOR THE ACTIVATED EVENT.
// see: https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerGlobalScope/activate_event
registration.addEventListener(‘activate’, function(event) {
// waiting for the ‘activate’ event to complete would be the
// same thing as listening for the non-existent ‘activated’ event to fire.
event.waitUntil(() => {
if (config && config.onActivated) {
config.onActivated(registration);
}
});
});
// OBJECTIVE 2.) SETUP PERIODIC UPDATE CHECKS.
// periodically poll for updates to the service worker
function checkUpdates(registration) {
if (registration && registration.update) {
registration.update();
setTimeout(function() { // queue up the next update check
checkUpdates(registration);
}, 1000 * 60 * 60 * AUTOMATIC_UPDATE_CHECK_INTERVAL);
}
}
// initiate periodic update checks.
checkUpdates(registration);
}[/code]
Note: At this point we’ve fully implemented the modifications that necessary to create automated updates with periodic checks in the background. The remaining code samples are only necessary for rendering announcements of these activities to the browser.
App.js
BEFORE
[code lang=”js” classname=”codeboxes” gutter=”false”] import React from ‘react’;
function App() {
return (
<React.Fragment>
<Head />
<BrowserRouter>
<Header />
<Routes />
<Footer />
</BrowserRouter>
</React.Fragment>
);
}
export default App;[/code]
AFTER. Note that I refactored the default App from a functional to a class component. This is necessary so that we can make use of the class component life cycle methods.
[code lang=”js” classname=”codeboxes”]
import React, { Component } from ‘react’;
import * as serviceWorkerRegistration from ‘./serviceWorkerRegistration’;
//
// misc app imports …
//
// UI stuff for service worker notifications
import AppUpdateAlert from ‘./components/appUpdate/Component’;
const UPDATE_AVAILABLE_MESSAGE = "New content is available and will be automatically installed momentarily.";
const SUCCESSFUL_INSTALL_MESSAGE = "This app has successfully updated itself in the background. Content is cached for offline use.";
class App extends Component {
constructor(props) {
super(props);
this.state = {
isSet: false, // True once componentDidMount runs
customClass: props.cls, // Expects "online" or "offline"
// service worker state management
// —————————————————————————————–
updatedSW: null, // The service worker that is waiting to be updated
isSWUpdateAvailable: false, // Set to True to trigger a Bootstrap alert.
// Set from a Workbox callback
// after a new service worker has
// downloaded and if ready to install.
wasSWInstalledSuccessfully: false // Set to True to trigger a Bootstrap alert.
// Set from a Workbox callback after
// service worker was successfully installed.
// —————————————————————————————–
};
// Workbox and React component callbacks.
// We want these bound to this class so that garbage collection
// never eliminates them while a Workbox event handler might
// call one of them.
this.resetSWNotificationStates = this.resetSWNotificationStates.bind(this);
this.onSWUpdateAvailable = this.onSWUpdateAvailable.bind(this);
this.onSWInstallSuccess = this.onSWInstallSuccess.bind(this);
}
// ——– Workbox Service Worker event handlers and state management ——-
// Callback for our AppUpdateAlert component.
resetSWNotificationStates() {
// this covers the intended use case
// of allowing a server worker update to proceed
// automatically, once the user has been made aware
// that the update exists, was downloaded in the background
// and is ready to install.
if (this.state.updatedSW && this.state.isSWUpdateAvailable) {
this.state.updatedSW.postMessage({
type: ‘SKIP_WAITING’
});
}
// reset the service worker states
this.setState({
updatedSW: null,
isSWUpdateAvailable: false,
wasSWInstalledSuccessfully: false
});
}
// Workbox callback for "service worker update ready" event
onSWUpdateAvailable(registration) {
if (this.state.isSet && registration) {
this.setState({
updatedSW: registration.waiting,
isSWUpdateAvailable: true,
wasSWInstalledSuccessfully: false
});
}
}
// Workbox callback for "service worker installation success" event
onSWInstallSuccess(registration) {
if (this.state.isSet) {
this.setState({
updatedSW: registration,
isSWUpdateAvailable: false,
wasSWInstalledSuccessfully: true
});
}
}
// ———— React Component life cycle methods ————
componentDidMount() {
this.resetSWNotificationStates();
this.setState({
isSet: true
});
// Note: I relocated this snippet from index.js
// in order to add Workbox’s two event handlers
// for onUpdate and onSuccess.
if (process.env.NODE_ENV === ‘production’) {
serviceWorkerRegistration.register({
onUpdate: this.onSWUpdateAvailable,
onSuccess: this.onSWInstallSuccess,
onActivated: this.onSWInstallSuccess // a custom event that I added to Workbox
});
}
}
render() {
// service worker app update alerts.
function AppUpdateAlerts(props) {
const parent = props.parent;
return(
<React.Fragment>
{parent.state.isSet &&
<React.Fragment>
{parent.state.isSWUpdateAvailable &&
<AppUpdateAlert
msg={UPDATE_AVAILABLE_MESSAGE}
callback={parent.resetSWNotificationStates} />
}
{parent.state.wasSWInstalledSuccessfully &&
<AppUpdateAlert
msg={SUCCESSFUL_INSTALL_MESSAGE}
callback={parent.resetSWNotificationStates} />
}
</React.Fragment>
}
</React.Fragment>
)
}
return (
<React.Fragment>
<Head />
<BrowserRouter>
<div className={"container-fluid p-0 " + this.state.customClass}>
<Header />
<AppUpdateAlerts parent={this}/>
<Routes />
<Footer />
</div>
</BrowserRouter>
</React.Fragment>
)
}
}
export default App;[/code]
appUpdate React Component
This React component is used in App.js. It renders a Bootstrap alert to the top of the screen which automatically disappears after 5 seconds and then fires an optional callback.
[code lang=”js” classname=”codeboxes” gutter=”true”]
import React from ‘react’;
import { Alert } from ‘reactstrap’;
import ‘./styles.css’;
const ALERT_VISIBILITY_SECONDS = 5.0;
class AppUpdateAlert extends React.Component {
constructor(props) {
super(props);
this.state={
visible : false,
msg: props.msg
}
// Bind the callback so we can execute
// it from anywhere inside this class, and
// so that garbage collection knows to
// leave it alone while we’re running.
if (props.callback) this.callback = props.callback.bind(this);
}
// make ourself `visible` so that the Bootstrap alert
// renders to the screen. Also set a Timeout to automatically
// fire after X seconds to automatically disappear the
// the alert as well as to execute the callback function.
componentDidMount() {
this.setState({
visible:true
}, ()=>{window.setTimeout(()=>{
this.setState({
visible:false
});
if (this.callback) this.callback();
}, 1000 * ALERT_VISIBILITY_SECONDS);
});
}
render() {
return(
<React.Fragment>
<div className="fixed-top m-0 p-5 text-right">
<Alert isOpen={this.state.visible} fade={true} className="border border-light alert alert-warning text-center">
{this.state.msg}
</Alert>
</div>
</React.Fragment>
);
}
}
export default AppUpdateAlert;
[/code]
I hope you found this helpful. Contributors are welcome. My contact information is on my web site. Please help me improve this article by leaving a comment below. Thank you!
Thank you very much for this, it helped me alot, thanks for sharing me
reactjs training in hyderabad
Thank you very much for this, it helped me alot, but i found that it only works on chrome-browser not chrome-mobile.
Thank you very much! Helps a lot!