DEV Community

loading...

Showing "new version available" notification on create-react-app PWAs

Danielly Costa
Hi! I'm a web developer who loves coding and it's passionate about learning new technologies.
・7 min read

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!


Gif of woman crying while using her laptop
— But the app works normally on my computer!! What's going on?!

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?
  2. Important things to notice before starting to code
  3. Finally, the implementation
    1. Creating the functions
    2. Placing snackbar provider
  4. Testing the feature

1. What happened to my app?

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

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

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

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

...
// 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

...
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.

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. 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

3. Finally, The implementation

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.


Men making a "don't understand" expression
— What?!

Calm down! I tried to drawn out for you
Diagram of the proposed solution

For making that possible, it will be used two states, two functions and notistack package, but you can use another package.

3.1. Creating the functions

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.

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.

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.

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

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

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.

//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.

Testing the feature

The last thing to do is testing the feature and to do so it's necessary to build the application. 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

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

Discussion (9)

Collapse
niketsoni profile image
Niket Soni

Hey Danielly, Thank you for this informative article. I have been following this article to convert my React app to PWA but when I am clicking on the refresh button I am getting an error.
Uncaught TypeError: e.postMessage is not a function.
Could you please help me with this?

Collapse
daniellycosta profile image
Danielly Costa Author

I'm so sorry not seeing your comment before... I think that maybe the setState function is not updating the state before calling the post message function ( reactjs.org/docs/state-and-lifecyc...).
Or if it's an old version, maybe the create-react-app doesn't create the service-worker file with postMessage function and you must configure the webpack configuration manually

Collapse
bakdakonusuruz profile image
Burak Bayraktaroglu

Check the state object that you save. If you are just saving "registration" object it will give you this error you should save "registration.waiting".
(That was my case, same error)

Collapse
emreerdogan profile image
Emre Erdoğan • Edited

Reloading the page with browser's reload button causes you to lose the snackbar and it doesn't appear again. As a workaround I used localStorage to keep the newVersionAvailable state and populate the local newVersionAvailable state on reload with the value from localStorage.

Collapse
khorengrig profile image
khorengrig

The same will happen if you close the tab do not click the refresh button and reopen the tab again.

Collapse
asdev808 profile image
asdev808 • Edited

I'm really struggling to understand the whole PWA thing...So I update my React App, build and deploy it, I open the website and in the console it says I need to close it and open it again to see the updates...I mean WTF...no regular user is going know that. Why cant the bloody thing just show the latest version, is that really too much to ask in 2021, sometimes I really hate web development.

Collapse
abhiak profile image
Abhinav Kaimal

This is not working in iOS and some android devices.

Collapse
bakdakonusuruz profile image
Burak Bayraktaroglu

For those who think it's not an iOS-friendly approach, don't be like Abhival. If you stalk him a little, you can see that he forked a repo. (github.com/Steviebaa/ios-friendly-...).
Check this out it may help.

Collapse
abhiak profile image
Abhinav Kaimal • Edited

First of all It's Abhinav!
Secondly PWA has a lot of limitations (Not diving deep into all of them). Talking about service worker updation, "new version available" won't work if your server has HTTP auth enabled, it took me some time to figure that out!