Showing "new version available" notification on create-react-app PWAs
Tags: cache, react, PWA, serviceWorker
Published: 2020-05-02 13:53:17

If you already received messages and print screens from your boss/selling team complaining that the app magically was replaced by a beautiful blank screen and you gently (after an internal panic attack, followed by sudden access to the production app for checking what's going on) explains that the solution is just reopening the page, maybe that post was made for you!

<center>
![Gif of woman crying while using her laptop](https://dev-to-uploads.s3.amazonaws.com/i/rh1cz8tkasyb5knba0id.gif)
*— But the app works normally on my computer!! What's going on?!*
</center>

Before starting I'd like to leave a message: 
*I wrote this article as a way to share what I found and made for solving new front end deployment problems on a system made using `create-react-app` package. I hope to help someone and if you have a better solution or a suggestion feel free to comment down below. It would be amazing to learn more with the community.*

## Table of Contents
1. [What happened to my app?](#what-happened)
2. [Important things to notice before starting to code](#important-things)
3. [Finally, the implementation](#implementation)
    1. [Creating the functions](#create-functions)
    2. [Placing snackbar provider](#snackbar-provider)
4. [Testing the feature](#testing)

## 1. What happened to my app? <a name="what-happened"></a>
To understand what happened, first of all, it is necessary to know some basic concepts as, what is *service worker* and how it is used on PWAs.

According to [Google Developers page](https://developers.google.com/web/fundamentals/primers/service-workers)

> A service worker is a script that your browser runs in the background, separate from a web page [...] The reason this is such an exciting API is that it allows you to support offline experiences, giving developers complete control over the experience.
> The intent of the service worker lifecycle is to:

> - Make offline-first possible.
> - Allow a new service worker to get itself ready without disrupting the current one.
> - Ensure an in-scope page is controlled by the same service worker (or no service worker) throughout.
> - Ensure there's only one version of your site running at once.


This offline experience on PWAs, similar to what we have on mobile apps is made by caching all of the static assets for a further visit. However, this could have some consequences due to the default service worker lifecycle behavior as explained on the [create-react-app documentation](https://create-react-app.dev/docs/making-a-progressive-web-app)

>After the initial caching is done, the service worker lifecycle controls when updated content ends up being shown to users. In order to guard against race conditions with lazy-loaded content, 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. 

So, when a new version of your app is deployed and a customer tries to access it, the browser is going to identify the new version but the customer will only access it on the next visit and depending on the changes made on the code the use of an old cached app could cause a blank pages. 

## 2. Important things to notice before starting to code <a name="important-things"></a>

If you take a look at the `index.js` of a `create-react-app` (CRA) project that uses service worker lifecycle you'll find something like this

```js
...
// 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://bit.ly/CRA-PWA
serviceWorker.register();
```
or even something like this, if you're using an old version of CRA

```js
...
if (process.env.NODE_ENV === 'production') {
 serviceWorker.register()
}
... 
```
and it's this function, `serviceWorker.register()`, that makes all the magic *behind the scenes*

Now, if you open `src/serviceWorker.js` file, what you're gonna see is a bunch of code verifying if the environment is the production one and checking if there is new content waiting to be load on the next visit or if the content is already updated and cached for offline use.

If the content on `serviceWorker.js` file seems to be complicated, don't worry! the *icing on the cake* is the discreet callback named `onUpdate(registration)` called when there is a new content waiting to be used. It's that callback we're going to use.

```js 
function registerValidSW(swUrl, config) {
...
            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.
              console.log(
                'New content is available and will be used when all ' +
                  'tabs for this page are closed. See https://bit.ly/CRA-PWA.'
              );

              // Execute callback
              if (config && config.onUpdate) {
                config.onUpdate(registration);
              }
            } else {
              ...
              }
            
    })
    ...
}
```

The last thing that's important to show could be viewed on the production version of your app, where a file named `service-worker.js` will be generated by [workbox](https://developers.google.com/web/tools/workbox/). This file could be seen on the `source` tab of the inspect option of your browser, as shown below.

As I said before, a new update found will be waiting to be load on the next access however, there is an option to skip this waiting time and the red arrow on the print screen is pointed to the event created for that.

![Print screen of service-worker.js on the Google Chrome browser ](https://dev-to-uploads.s3.amazonaws.com/i/axuazvil7aoxos8o0m44.png)

## 3. Finally, The implementation <a name="implementation"></a>

Once we found such things, we need to implement a feature that sends a `SKIP_WAITING` message for the service-worker on the browser when there is a new version available and the user clicks on an update button on a snackbar notification shown on the interface.

<center>
![Men making a "don't understand" expression](https://dev-to-uploads.s3.amazonaws.com/i/gng9gflqriocfkwjf6q7.gif)
*— What?!*
</center>

Calm down! I ~~tried to~~ drawn out for you
![Diagram of the proposed solution](https://dev-to-uploads.s3.amazonaws.com/i/fnl69f8pro3m2k82qbi6.png)

For making that possible, it will be used two states, two functions and [`notistack`](https://www.npmjs.com/package/notistack) package, but you can use another package.

### 3.1. Creating the functions <a name="create-functions"></a>

Assuming that a new version could be detected in any route of the app, any of these routes may be able to trigger the skip message to the service-worker event listener when the app loads, so we're going to make the functions and the states on the main application component, the `App.js`.

*Notice that the approaching will depend on your project. The project I was working on has the `App.js` component as a `class` but feel free to use  React Hooks if you're using functional components.*

The first needed states are a boolean to manage the opening of the snackbar and an object for saving the waiting service worker. I named them `newVersionAvailable` and `waitingWorker` respectively and these states are going to be changed through the `onUpdate` callback called when the browser finds out another version of the app. I named this callback `onServiceWorkerUpdate()` as can be seen on the code block below.

```js
onServiceWorkerUpdate = registration => {
		this.setState({
			waitingWorker: registration && registration.waiting,
			newVersionAvailable: true
		})
	}
```

The next function declared and showed below was the `updateServiceWorker()`, that posts the `SKIP_WAITING` message and will be used on the snackbar refresh button.

```js
updateServiceWorker = () => {
		const { waitingWorker } = this.state
		waitingWorker && waitingWorker.postMessage({ type: 'SKIP_WAITING' })
		this.setState({ newVersionAvailable: false })
		window.location.reload()
	}
```

Besides the addition of these functions, the `serviceWorker.register()` should be cut from `index.js` and pasted on the `App.js`. This register function needs to be executed on the first load of the application and we also need to pass `onServiceWorkerUpdate()` function, made previously, as an argument to it as well as use `updateServiceWorker()` as the snackbar `onClick` function as you can see on the next code block.

```js 
componentDidMount = () => {
    const { enqueueSnackbar } = this.props;
    const { newVersionAvailable } = this.state;

if (process.env.NODE_ENV === 'production') {
    serviceWorker.register({ onUpdate: this.onServiceWorkerUpdate });
}

    if (newVersionAvailable) //show snackbar with refresh button
      enqueueSnackbar("A new version was released", {
        persist: true,
        variant: "success",
        action: this.refreshAction(),
      });
  };

refreshAction = (key) => { //render the snackbar button
    return (
      <Fragment>
        <Button
          className="snackbar-button"
          size="small"
          onClick={this.updateServiceWorker}
        >
          {"refresh"}
        </Button>
      </Fragment>
    );
  };
``` 

With these modifications made, the `App.js` should look like this

```js
import React, { Component, Fragment } from "react";
//default imports...
import { withSnackbar } from "notistack";
import * as serviceWorker from "./serviceWorker";
import { Button } from "@material-ui/core";

class App extends Component {
  constructor(props) {
    super(props);
    this.state = {
      newVersionAvailable: false,
      waitingWorker: {},
    };
  }

onServiceWorkerUpdate = (registration) => {
    this.setState({
      waitingWorker: registration && registration.waiting,
      newVersionAvailable: true,
    });
  };

  updateServiceWorker = () => {
    const { waitingWorker } = this.state;
    waitingWorker && waitingWorker.postMessage({ type: "SKIP_WAITING" });
    this.setState({ newVersionAvailable: false });
    window.location.reload();
  };

  refreshAction = (key) => { //render the snackbar button
    return (
      <Fragment>
        <Button
          className="snackbar-button"
          size="small"
          onClick={this.updateServiceWorker}
        >
          {"refresh"}
        </Button>
      </Fragment>
    );
  };


  componentDidMount = () => {
    const { enqueueSnackbar } = this.props;
    const { newVersionAvailable } = this.state;
if (process.env.NODE_ENV === 'production') {
    serviceWorker.register({ onUpdate: this.onServiceWorkerUpdate });
}

    if (newVersionAvailable) //show snackbar with refresh button
      enqueueSnackbar("A new version was released", {
        persist: true,
        variant: "success",
        action: this.refreshAction(),
      });
  };

  render() {
       //render components
   }
}
export default withSnackbar(App); //uses the snackbar context

```


### 3.2. Placing snackbar provider <a name="snackbar-provider"></a>

Once the main App component is ready, the file `index.js` is the next one to be changed.

On this file, it is necessary to wrap the main App component with the Snackbar provider.

```js
//default imports
import { SnackbarProvider } from "notistack";

ReactDOM.render(
  <React.StrictMode>
      <SnackbarProvider>
          <App/>
      </SnackbarProvider>
  </React.StrictMode>,
  document.getElementById("root")
);
``` 

If you have questions about `notistack` package I recommend accessing [this site](https://iamhosseindhv.com/notistack/demos).


## Testing the feature <a name="testing"></a>

The last thing to do is testing the feature and to do so it's necessary to [build the application](https://create-react-app.dev/docs/deployment/). For that, you can use the command below.

```
npm run build
```

For handling the static server in an easier way, I recommend using the `serve` package that can be installed and executed using the following commands.

```
npm install -g serve
serve -s build
``` 
After running these commands, the application will be running (probably on 5000 port) and if you open the console you should see some logs like these

![Application running with caching enable](https://dev-to-uploads.s3.amazonaws.com/i/gizgs2n71uts4kmrt985.png)

Now, back to the code and make some modifications, for example, change the button label. Now execute `npm run build` again and refresh the page served. The result should be something like the GIF below.

![New version notification appearing on the screen ](https://dev-to-uploads.s3.amazonaws.com/i/powoo4ao9l0gdunk30sc.GIF)