PWA: Communicating Between the Frontend & the Service Worker
Published: 2023-06-19 14:59:17

In a [previous article](https://dev.to/pipoprods/making-a-simple-pwa-34l8) we saw how to create a basic PWA that works offline. This PWA had a Service Worker that was transparently issuing requests for the frontend, returning cached data if network was unreachable.

This article will go deeper into the PWA world and we'll make both the frontend and the Service Worker talk to each other.

The code examples in this article are based on the code written for previous article. The whole code is available here: [https://code.pipoprods.org/web/simple-pwa](https://code.pipoprods.org/web/simple-pwa).

## Making the Service Worker instantly control the page

You probably noticed that once installed, the Service Worker was not working for the page that triggered install.
That's [an expected behavior](https://github.com/w3c/ServiceWorker/blob/main/explainer.md#so-im-controlling-pages-now).

Fortunately, the API gives us a method to [set the Service Worker active for all the clients it is attached to](https://www.w3.org/TR/service-workers/#dom-clients-claim).

Let's make our Service Worker claim its clients:

```javascript
/**
 * Then take immediate control on the page that triggered the installation
 */
self.addEventListener('activate', () => {
  self.clients.claim();
});
```

## Detecting online/offline & loading state

Next step, we'll implement a message channel between the Service Worker and its clients.
This channel will be used to broadcast messages to update the UI according to network status.

On the Service Worker side, create a communication channel:

```js
const appCacheName = 'simple-pwa';
const appContentToCache = [
  '/',
  'index.html',
  'app.js',
  'favicon.ico',
  'manifest.webmanifest',
  'icons/icon-512.png',
];
const dataCacheName = `${appCacheName}-data`;

/*-----8<-----8<-----8<-----8<-----*/
// Communication channel with clients
const channel = new BroadcastChannel('sw-messages');
/*-----8<-----8<-----8<-----8<-----*/
```

Then write messages when something needs to be reported to the frontend:

```js
 async function fetch_resource(resource) {
   response = await get_from_cache(resource, appCacheName);
   if (response) {
     return response;
   } else {
/*-----8<-----8<-----8<-----8<-----*/
     channel.postMessage({ loading: true });
/*-----8<-----8<-----8<-----8<-----*/
     try {
       response = await fetch(resource);
       await put_into_cache(resource, response);
/*-----8<-----8<-----8<-----8<-----*/
       channel.postMessage({ loading: false });
       channel.postMessage({ offline: false });
/*-----8<-----8<-----8<-----8<-----*/
       return response;
     } catch (error) {
       // TODO: check if error is because we're offline
       response = await get_from_cache(resource);
/*-----8<-----8<-----8<-----8<-----*/
       channel.postMessage({ loading: false });
       channel.postMessage({ offline: true });
/*-----8<-----8<-----8<-----8<-----*/
       if (response) {
         // resource was found in data cache
         return response;
       } else {
         return new Response('offline', {
           status: 408,
           headers: { 'Content-Type': 'text/plain' },
         });
       }
     }
   }
 }
```

On the frontend side, let's add elements to our HTML:

```html
   <body>
     <h1>Simple PWA</h1>

     <div class="controls">
       <button onClick="previous()">&lt;</button>
       <span id="api_id"></span>
       <button onClick="next()">&gt;</button>
     </div>

<!-----8<-----8<-----8<-----8<----->
     <div id="offline">
       You're currently offline. The data below may differ from the current
       online version.
     </div>
 
     <div id="loading">loading...</div>
<!-----8<-----8<-----8<-----8<----->
 
     <pre id="result"></pre>
   </body>
```

Style them:

```css
      html,
      body {
        height: 100%;
        width: 80%;
        margin: 0;
        display: flex;
        flex-direction: column;
        margin-left: auto;
        margin-right: auto;
        font-family: sans-serif;
      }

      h1,
      div.controls {
        text-align: center;
      }

      #api_id {
        font-family: monospace;
        text-align: center;
        display: inline-block;
        width: 5em;
      }

/*-----8<-----8<-----8<-----8<-----*/
      #offline {
        border: 1px solid red;
        background-color: rgba(255, 0, 0, 0.1);
        padding: 5px;
        text-align: center;
        color: red;
        margin-top: 10px;
        margin-bottom: 10px;
        visibility: hidden;
      }

      #loading {
        text-align: center;
        visibility: hidden;
      }

/*-----8<-----8<-----8<-----8<-----*/
      pre {
        display: flex;
        margin-left: auto;
        margin-right: auto;
      }
```

Attach to the communcation channel:

```js
document.addEventListener('DOMContentLoaded', () => {
  fetch_data(1);
  if ('serviceWorker' in navigator) {
/*-----8<-----8<-----8<-----8<-----*/
    const channel = new BroadcastChannel('sw-messages');
    channel.addEventListener('message', (event) => handle_message(event.data));
/*-----8<-----8<-----8<-----8<-----*/
    navigator.serviceWorker.register('sw.js');
  }
});
```

Finally, handle the incoming messages:

```js
function handle_message(message) {
  if (message.offline !== undefined) {
    document.getElementById('offline').style.visibility = message.offline
      ? 'visible'
      : 'hidden';
  }

  if (message.loading !== undefined) {
    document.getElementById('loading').style.visibility = message.loading
      ? 'visible'
      : 'hidden';
    document.getElementById('result').style.visibility = message.loading
      ? 'hidden'
      : 'visible';
  }
}
```

The UI will display a warning when offline:


![Offline indicator](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/zwc0x5f0btcewhlhb7ce.png)


And a loading indicator:

![Loading indicator](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/bxn4qsqr0fws27zzhk19.png)

## Final thoughts

In this article, we implemented a simple broadcast communication between the Service Worker
and its clients. This could be used for more complex things:

- pass data from a client to another (multi-window app)
- make navigation more dynamic returning cached API data first, then updating it in the background

The source code is here:
[https://code.pipoprods.org/web/simple-pwa](https://code.pipoprods.org/web/simple-pwa)