Accessibility testing.

The challenges of Progressive Web Apps - How to refresh progressive web application

Posted by Paweł Głogowski on December 20, 2019

Progressive Web App refreshing problem?

This is the second part of our series about PWA. If you’d like to learn how to change your web page into a Progressive Web Application, be sure to check out our previous article. Solidstudio Software House built the T-Cup application that can be served as a Progressive Web Application. We follow a CI/CD approach to enable quite frequent changes in the product and ensure that our users get the latest content of the application.

Since a Progressive Web Application is heavily cached, serving the latest content to users can be challenging. It’s not possible to refresh the application when you’re in standalone mode so we have to do it differently. We spent some time on preparing a user-friendly solution that would allow users to get the latest version of the application quickly.

Different ways to refresh your application

Developers can approach the problem of refreshing the application in different ways. The easiest way could be adding a fixed refresh button that is available only on the Progressive Web Application version. It might be the easiest way since its doesn’t require any interaction with service workers. However, it’s not very user-friendly. Users may not be aware that the latest version of the application is already available and ignore that button.

The second approach could be informing users when the application is showing on the screen. So once the new version is available, users will see some indicator bar with a refresh button. That way, they will know that there’s new content to fetch from server. Let’s take a look at how that’s indicator bar could look like.

Refresh bar sample

We can achieve the above effect by using material-ui. Take a look at the following code to see how to achieve it. To get the best result, you should show this bar only to users in a standalone mode (PWA) - we discuss this in our previous article (link) so make sure that you generate that bar only in standalone mode. Additionally, you can style your snack bar to suit your application best.

<SnackbarContent
   id="refresh-bar"
   className="refresh_bar"
   style={{display: 'none'}}
   message={
       <Button id="refresh_btn" key="install" aria-label="close" color="inherit"
               className="refresh_bar_class" onClick={this.onClose}>
           New Content available. Please restart the application!
       </Button>
   }
   action={[
       <IconButton key="close" aria-label="close" color="inherit" onClick={this.close}>
           <CloseIcon fontSize="small"/>
       </IconButton>
   ]}
/>
                        
You can see two handlers in above code - let's see how they can looks like:
close = () => {
   let infoBar = document.querySelector("#refresh-bar");
   if(infoBar) {
       infoBar.style.display = 'none';
   }
}
                

We don’t want to allow refreshing the application because we notice some unexpected behaviour on iOS - it tends to fail randomly or behaves in an unstable way after some refreshing, so we encourage users to kill the app and reopen it. However, refreshing seems to be stable on Android and Chrome, so for Chrome users you can change the bar and show the refresh icon so users can refresh app to get new content on their own.

From the programmatic perspective, nothing interesting is done here - we just show a bar and allow user to close it. The more important thing here is how we indicate that a new version of the application is available to the user. To do that, we need to define a proper callback for updating service workers and force them to be updated. More on that in the next part of the article.

Custom Service worker update callback

Let's take a look at serviceworker configuration generated during the development of a React application.

function registerValidSW(swUrl, config) {
   navigator.serviceWorker
       .register(swUrl)
       .then(registration => {
           registration.onupdatefound = () => {
               const installingWorker = registration.installing;
               if (installingWorker == null) {
                   return;
               }
               installingWorker.onstatechange = () => {
                   if (installingWorker.state === 'installed') {
                       if (navigator.serviceWorker.controller) {
                           // We will use this callback to present button to allow user to refresh 
                           // the application

                    // Execute callback
                           if (config && config.onUpdate) {
                               config.onUpdate(registration);
                           }
                    
                    } else {
                           // Not necessary for this example
                       }
                   }
               };
           };
       })
       .catch(error => {
           console.error('Error during service worker registration:', error);
       });
}
                

I have highlighted the most important parts of that code snippet. As you can see, we add a listener on the state change of service worker. It will be executed when changes occur in service worker. It might be misleading that we’re watching for the ‘installed’ state. But the existence of the controller property indicates changes in the service worker. What we need to do is to pass config object with callback onUpdate function. We usually need to register our service worker in the index.js file. Here we pass object with onUpdate property which is a callback function.

serviceWorker.register({ onUpdate: (registration) => {
       let refreshBar = document.querySelector('#custom-refresh-bar');
       if(refreshBar) {
           refreshBar.style.display = '';
       }}});
                        

Making your application usable in case of the lack of an internet connection isn't going to be a part of this article. Instead, we will focus on enabling users to install your application for both iOS and Android applications.

Skip waiting for new service worker

However, it's not enough for the configuration of service worker. Because we want the new service worker to take over old one immediately, we need to configure it to skip waiting. If we don’t do this, the new service worker will be available after the user navigates the refresh current page. It's not always an option since we observe some mayor problems with refreshing page in the PWA mode. Visit the following page to understand the service worker lifecycle better.

To configure the service worker to skip waiting, we need to add some code to the default service worker implementation which is provided when we generate sample project. To do so, we will use a tool called cra-append-sw. So we first need to add a proper dependency to the package.json file along with the modification of build command. It should look like this:

react-app-rewired build && cra-append-sw ./src/custom-sw.js
                    

Note: The following example is valid for React application, but it will look quite similar for application build in other technologies.

You need to provide code for your custom service worker. For now, it will be very simple and will only contain the service worker skip waiting logic. This code will be appended for the actual service worker code.

self.addEventListener('install', event => {
   self.skipWaiting();
});
                    

It's not much code, but right now we can be sure that the new service worker will take over the old one.

Update service worker

So far, it all looks quite straightforward, but we need the most important piece of the whole picture - we need to know when the changes to our web application occur. In order to do this, we need to force our service worker to “update”. The name for that operation may be confusing because it’s not update itself - rather, it’s checking whether the new version of the application is available. Let's take a look at this piece of code:

export function forceSWupdate() {
    if ('serviceWorker' in navigator) {
        navigator.serviceWorker.ready.then(registration => {
            registration.update().then(() => {
 
            });
        });
    }
 }            
                      

This will download the newest version of the service worker file from the server and compare it with the current version to detect any differences. It will fire an event that we can handle by installingWorker.onstatechange which was described previously. You may ask how application “knows” that there were changes in code. When you take a look at the build directory, you can see a file like this.

If you see the content of this file, you will notice something like this:

 {
   "revision": "04dcc2f53ec32ae36c2c",
   "url": "/static/js/1.29c95ede.chunk.js"
 },
 {
   "revision": "437c5ba5eba948b3918c",
   "url": "/static/css/2.6386ac78.chunk.css"
 }

                

When any file changes, then in precache file there will be different revision numbers and name of precache file hash ( in the name ) changes. This file is included in the service worker file so if name is changes, then the service worker file changes so the service worker can notice that change.

Adding a PWA to the home screen in iOS

The last thing we need to cover is determining when to check for the update. Of course, using the setTimeout() feature available in React allows to periodically check for updates of the new version. But this approach can cause some unexpected issues and unnecessary performance costs so we believe that it’s not enough.

We decided to take advantage of the PWA lifecycle and web API visibilitychange feature, so once users display the main screen of the application, we catch that event and force the service worker to update. Let's take a look at the following code:

document.onvisibilitychange = (e) => {
   let refreshBar = document.querySelector('#refresh-bar');
   if(refreshBar && !document.hidden) {
       serviceWorker.forceSWupdate();
   }
}
                    

Note: This approach may not work on Google Chrome. If you’re using Chrome, consider different events like scrolling or clicking a button.

We can catch the visibility change event which is fired when the application is displayed on the user’s screen. However, this event is fired when the app is shown and when it’s hidden so we need to check what the value of the hidden property is to determine if it’s displayed.

So now we have all the pieces fitting together and the final flow looks like this: The user opens PWA --> PWA catches the visibility change event and forces the service worker to update → the service worker checks if a new version of the application is available → we handle the update event by showing the refresh bar to the user → The user restarts the app (iOS) or refreshes it (Android) to get new content.

You can see that there are some differences between iOS and Android. To help you understand that better, let’s take a look at the following table.

Android(Chrome) iOS(Safari)
Allows to refresh application with button - you can safely show refresh button to the users. Better to not follow refresh approach and encourage users to restart application.
Use focus event listener or other supported listeners to handle application opens. Use web api ‘onvisiblechange’ events to handle open of application.

Summary

As you can see, there are a lot of different pieces involved in the process of refreshing your PWA. But in the end, it will be very beneficial for your users. At Solidstudio Software House, we followed this approach in our product T-Cup to make sure that users have the latest version of the application. In our experience, Chrome seems to have a more stable and reliable environment when it comes to supporting PWA and we still need to fix some unexpected issues for iOS.

Let`s work together!

+48 538 365 618