Background sync in Progressive web apps (PWA)

Progressive web apps (PWA) allow users to add the web app to the home screen. The goal is to make a web app look like a native app. So, a PWA is responsive and adjusts well to appear good in any mobile device. Even when the device is offline, it should render some content. Optionally, it should respond to push notifications, sync the app content in the background and have the capability to access camera, location, etc.

Having a manifest json file in the web app allows the browser to add the app to the home screen. In addition to the manifest json file, the app should also have a service worker.

A service worker implements most of the PWA features: offline caching, background sync and push notifications. It runs in the background even when the user closes the app or the browser.

In this article, we are going to build an example PWA that has the background sync capability. Create react app (CRA) provides a template for building PWAs. The basic template has the app manifest (JSON file) with relevant properties set. It also has a service worker. The service worker in the CRA template uses a library named Workbox. Workbox provides a lot of capabilities out of the box. In this article, we will show how to implement background sync without Workbox. But we will implement background sync with a Workbox plugin.

A) Background Sync – Example app

In 2015 before PWAs became popular, I built a mobile app for data collection at hospitals. An agent will receive information about the hospital. Then he would go to the hospital and collect data. The app sends the collected data to a backend server.

In some hospitals, the network connectivity was very poor. So poor that the app does not send the data to the server. So, we built an offline storage capability within the app. When the network is poor, the app saves the data in a database. And when the network is back, the app will send all the data to the backend.

With PWAs, we don’t need to build such an app. A PWA should work when the network is down. It caches assets like JavaScript, CSS, images in the browser cache. With some custom code, the PWA should cache app data in indexedDB. IndexedDB is a database built within all modern browsers that persists data even when the user closes the browser. With caching and background sync feature, we can build a PWA that collects data when offline and sends it to the server when the user is online.

App overview

For this article, we will build the example app from scratch. I won’t highlight the React specific portions of the app. But it is available in this git repo. Let’s look at the screenshots of the completed app.

Hospital listing page
Hospital listing page

When the agent logins to the app, he sees a list of hospitals for which he has to collect data. On clicking Edit, he sees the hospital edit page shown below.

Hospital edit page
Hospital Edit page

For our example app, we allow the agent to edit only four fields: The number of specialities in the hospital, the number of doctors in the hospital, the number of surgeries in the hospital and the total beds available in the hospital.

When the user enters some value and hits update, the app should send the data to the backend. If there is no network, the app stores the data in indexedDB and sends it whenever network becomes available.

A note on the server

There is a NodeJS server that I have put together for this app. You can view the server code from the github repo. There are two endpoints: ‘GET /hospitals’ and ‘POST /hospital/:id’. For the hospitals endpoint, it picks data from a ‘hospitals.json’ file and sends it as an array. When the user updates hospital data, the app sends it to the other endpoint. The hospital endpoint saves the data to the same ‘hospitals.json’ file.

B) PWA using CRA

Create a new progressive web app using the CRA template like so:

npx create-react-app pwa-demo --template cra-template-pwa

The CRA template provides a manifest.json in the public folder. Customise the properties by providing a name, short name. The short name is what appears as the app name when the user adds the PWA to the home screen.

    "short_name": "Data collection app",
    "name": "Hospital data collection app",

CRA template intimates the browser where to find the app manifest by specifying a link tag in index.html.

<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />

There is also a service worker (src/service-worker.js) and a service worker registration file (src/serviceWorkerRegistration.js). When we build the app, the build process compiles the service worker file and outputs it in the build folder. So, testing service worker should happen after we do a build. In addition, we should change some code in src/index.js file.

serviceWorkerRegistration.register();

Change the code from “unregister” to “register”. So, every time we run the app, the app registers the service worker. The browser checks if the contents of the service worker file has changed. If the service worker file has changed, the browser uninstalls the previous service worker and installs the new service worker. To ensure that the whole process is robust, the browser activate the new service worker only when the user closes the app.

Service worker development workflow

Perform the following steps when you are working with the service worker file:

  1. Build the app (yarn build).
  2. Run the built app using serve (or a similar package). (serve ./build)
  3. Open localhost:5000.
  4. Go to Applications tab in developer tools.
  5. Open the service worker section.
  6. Click on the “skip waiting” link next to the service worker.
  7. Refresh the page.
skipWaiting link in service worker section
skipWaiting link in service worker section

Offline caching using Workbox

What does the default service worker provided by CRA template do? It knows the list of static assets like JavaScript, CSS, etc that the build process generates. And it caches all those assets in the browser cache. The following line of code does that.

precacheAndRoute(self.__WB_MANIFEST);

Open applications tab and click on the cache section. Open the “workbox-precache-v2-<appName>” cache. You should see all the static assets in there.

Workbox precache
Workbox precache

We can access the Cache using the Cache API. Workbox does that for us. But the code should look like so:

caches.open('workbox-precache-v2').then(cache => {
  cache.add(request);
});

The above code will store the request URL as the key and the response from the server as the value.

C) Saving data in indexedDB

We already have a progressive web app template project. So, let’s enhance it with some code. We will build the hospital listing page. This page fetches the list of hospitals from the API and displays it as a card.

useEffect(() => {
    fetchHospitals();
}, []);

The function fetchHospitals make a network request to the API server.

function fetchHospitals() {
    axios
        .get('http://localhost:4000/hospitals')
        .then((response) => {
            setHospitals(response.data);
        })
}

When the network is available, it works perfectly. But what happens when the network is down? We want some way to cache API data when there is no network. For storing app data, we make use of indexedDB.

function fetchHospitals() {
    axios
        .get('http://localhost:4000/hospitals')
        .then((response) => {
            const dbOpenRequest = indexedDB.open('hospitalDB', 1);
            dbOpenRequest.onupgradeneeded = function (event) {
                const db = event.target.result;
                db.createObjectStore('hospitalStore', { keyPath: 'id' });
            };
            dbOpenRequest.onsuccess = function (event) {
                const db = event.target.result;
                const txn = db.transaction('hospitalStore', 'readwrite');
                const store = txn.objectStore('hospitalStore');
                const clearRequest = store.clear();
                clearRequest.onsuccess = function () {
                    response.data.forEach((hospital) => {
                        store.add(hospital);
                    });
                };
            };
            setHospitals(response.data);
        })
        .catch(() => {
            const dbOpenRequest = indexedDB.open('hospitalDB', 1);
            dbOpenRequest.onsuccess = function (event) {
                const db = event.target.result;
                const txn = db.transaction('hospitalStore', 'readonly');
                const store = txn.objectStore('hospitalStore');
                const getAllRequest = store.getAll();
                getAllRequest.onsuccess = function () {
                    setHospitals(getAllRequest.result);
                };
            };
        });
}

The above code saves the API data in a database with name “hospitalDB”. Within hospitalDB, there is an object store (similar to a table) with name “hospitalStore”. We remove all entries within the object store and populate it with fresh data from the API.

When the network is down, the axios call throws an exception. In the catch handler, we open the same “hospitalDB” and retrieve the data from “hospitalStore”. In this way, the hospital listing page is available when the network goes down.

D) Background sync without Workbox

It is time to work on the “Update hospital” form. Background Sync is available only in Chrome. There is no support for Background sync in Safari. So, it won’t work with an iPhone. But since this is a data collection app and we can tell the agents to use Chrome on Android, it won’t be a problem.

The code when the user submits the update form looks like so:

function handleSubmit(e) {
    e.preventDefault();
    axios
        .post(`http://localhost:4000/hospital/${activeId}`, hospitalData)
        .then(() => {
            setActiveId(null);
            fetchHospitals();
        })
        .catch(() => {
            // store the form data in indexedDB
            // trigger a sync task
            navigator.serviceWorker.ready(serviceWorkerRegistration => {
               serviceWorkerRegistration.sync.register('some-unique-tag');
            });
        });
}

When the network is down, the axios call to the API fails and we trigger the catch handler. In the catch handler, we store the form data somewhere in indexedDB and register a sync task. To register a sync task, we need the service worker registration object. The register method needs a unique tag to identify the sync task.

Since our API is in localhost, how do we simulate network failure? Simply kill the server. If there is no server running on localhost:4000, then the axios call will trigger an exception.

The browser checks when the network is available. If the network is available, it triggers a sync event on the service worker. So, we can handle the sync event in the service worker like so.

self.addEventListener('sync', event => {
  if (event.tag === 'unique-tag-name') {
    // retrieve data from indexedDB
    // make the api call with the data
  }
});

In the sync event handler, we check if the sync event has the right tag. If so, we retrieve the form data from the indexedDB and make an API call with that data.

Saving data to indexedDB and retrieving data from it can be a pain. Fortunately, Workbox does all the heavy-lifting. With just a few lines of code, we can achieve background sync with workbox. That is the approach that we will follow in this article.

E) Background sync with Workbox

There are only four lines of code we have to write in our service worker to make background sync work.

Import the background sync plugin:

import { BackgroundSyncPlugin } from 'workbox-background-sync';

Create a new instance of the plugin with a queue name.

const bgSyncPlugin = new BackgroundSyncPlugin('hospital-queue');

Import the NetworkOnly strategy at the top of the file.

import { StaleWhileRevalidate, NetworkOnly } from 'workbox-strategies';

Intercept the API call to “POST /hospital/:id” with the background sync plugin.

registerRoute(
    /.*\/hospital\/.*/,
    new NetworkOnly({
        plugins: [bgSyncPlugin],
    }),
    'POST'
);

The four lines of code does a lot of work for us. Workbox has a “registerRoute” method. This method intercepts a fetch request. The fetch request can be anything: request for a HTML page, style file or JavaScript file or an API request. In our case, we ask Workbox to intercept the API request to “/hospital/:id”. When workbox intercepts a request, we provide a handler function to do something with the request. For example, the following code will work alright as we provide a valid handler.

registerRoute(
    /.*\/hospital\/.*/,
    args => console.log(args),
    'POST'
);

But in our case, we provide a strategy as handler. Strategies allow us to specify how we fulfil a request. We can fulfil a request by making a network request or retrieving a response from cache or both. In our case, we fulfil the request by making a network call only. And when we do the network call, we also provide the background sync plugin that will do some additional work.

The background sync plugin does some super-useful work. It checks if the network call fails. In case of network call failure, the plugin saves the request data in indexedDB. The following screenshot shows that.

Background sync in action
Background sync in action

In our case, we simulate network failure by not running the API server. Note that the plugin creates a DB with name “workbox-background-sync” and an object store with the name “hospital-queue”. This is a name that we provide to the plugin.

The plugin also registers for a sync task with the name of “workbox-background-sync:hospital-queue”.

When the network is available, the browser triggers a sync event on the service worker. Workbox gets the data from the indexedDB and performs the API request with that data.

In our case, the browser does not trigger a sync event automatically. This is because our API server is down. So, we manually trigger the sync task from the service worker section.

Trigger sync task
Trigger sync task

The source code is available in a github repo. In the server folder, Install all dependencies using “yarn”. Then start the server using “yarn start”. In the client folder, install all dependencies. Build the client using “yarn build”. Run “yarn serve build” to run the production build of the app. Open localhost:5000 for accessing the fully functional progressive web app. Hope this article is useful. And if something does not work, please leave a comment.

Related Posts

12 thoughts on “Background sync in Progressive web apps (PWA)

  1. Very informative writing on starting with PWA. Also instead of killing the server for simulating a network failure, I would suggest using the throttling controls under ‘Network’ tab for simulating network failure.

  2. Great article. I came to your article, after searching for a way to extend a PWA Plugin I am using for Mythos WordPress website. The goal will be to add background sync but most importantly, precaching and offline Pages and media.

  3. This is an excellent article. However, I’m a little confused about the demo. Does this demo just show what you described related to using Background Sync without Workbox?

    I was trying to see the example related to Workbox.

    1. The demo / github repo shows background sync using Workbox. Background sync without Workbox is explained only as additional information.

      1. So you still have to implement the IndexedDB interaction even with Workbox? I was under the impression Workbox did all that for you automatically based on your article. Or did I misunderstand?

        1. There is no need to use indexedDB for the Workbox implementation. Adding the plugin to the route handler / strategy is sufficient.

    1. In the article, I am explaining how to cache dynamic data (API response) in indexedDB and showing it from there when the browser is offline.

Leave a Reply

Your email address will not be published.