We're planting a tree for every job application! Click here toĀ learn more

Notify A Progressive Web App (PWA) Updates

Ahmad Atallah

22 Apr 2020

ā€¢

6 min read

Notify A Progressive Web App (PWA) Updates
  • JavaScript

Offline/Cache-first behavior is one of the main key features in any Progressive Web Apps, but according to create-react-app docs: "the offline/cache-first behavior is opt-in only. By default, the build process will generate a service worker file, but it will not be registered, so it will not take control of your production web app.", this will get you in a very disturbing state when your app is already built without registering service worker, and deployed to a group of people who are using it regularly. Disturbing because users will not notice your updates if you decided to register the service worker unless they explicitly hard reload the browser or even close all tabs. That's why I think that registering service worker should be a proactive decision because it will affect the users who almost aren't familiar with offline-first apps. So, if you take a decision to go on with registering service worker, it will be better to handle this from the first release of your app then users will get update message everytime your app updated. In this article I will discuss how to register service worker correctly in your create-react-app Progressive Web Apps and notify users with any update goes live.

The solution requires a basic knowledge about service worker, react, redux and react-redux the official react bindings for redux.

Create React App insights

I will reference Create React App documentation in this section just to get you familiar with offline first apps.

ā€¢ [Faster and reliable](https://create-react-app.dev/docs/making-a-progressive-web-app# why-opt-in): Offline-first Progressive Web Apps are more faster and reliable than traditional apps because it actually caches all the static assets that can be served by your app regardless of the network connectivty. Also, using manifest.json file located in the project public directory to add a mobile app version of the app without any need to install it from the store. Defining your icon, name, and start_url of your project are the main configurations for a mobile-app like view.

ā€¢ [Require HTTPS](https://create-react-app.dev/docs/making-a-progressive-web-app# offline-first-considerations):

Service workers require HTTPS. That's why it doesn't apply to localhost, not recommended in development environment, and it can only be applied in production environment. So, better to test it by serving the build directory using serve npm package.

  npm install -g serve

ā€¢ [No interception with cross-origin resources](https://create-react-app.dev/docs/making-a-progressive-web-app# offline-first-considerations):

The generated service worker doesn't intercept any cross-origin resources like HTTP API requests.

Registering service worker

If you decided to go with registering service worker in create react app, you should register it in index.js by changing serviceWorker.unregister() to serviceWorker.register(). However, this will not guarantee any updates to be sent to users as the previous service worker will serve the older content, unless the user hard reloads the browser or close all opened tabs. What we need is to notify the user that the browser should be hard reloaded, in case he don't want to close all opened tabs, or even close all tabs opened.

Notify

This is the major step in our setup which includes adding:

  • Redux store that will contain the service worker state.
  • On-update callback to be passed to service worker registeration.
  • Notification component; I will use react-toastify.

Service worker state

Configure store

We will handle service worker state across our PWA by configuring a store that runs only in production environment. There is a common practice concerning the configuration of redux store which recommends creating store directory into src home directory. This directory should contain two separate store configurations; one for development environment and another for production. Here we only need a production configuration that handle the service worker state.

/* connfigure-store.js */

import { createStore } from "redux";
const configureStore = (initialState = { serviceWorkerUpdated: false }) => {
  return createStore(/*root reducer*/, initialState);
};

export default configureStore;

In the code snippet above, the initialState is just an object with a serviceWorkerUpdated state set to false and that's it. We imported the createStore from redux passing the initialState only for now as we will create the root reducer in the next step.

Create service worker reducer

Creating a reducer requires defining actions that aimed to be dispatched for specific input from the app. In our case, we want to dispatch an action called UPDATE_SERVICEWORKER once there is any waiting service worker, informing the user that this app is being served cache-first and there is new content that will not be shown unless you closed all browser tabs.

/* service-worker-reducer.js */

// CONSTANTS
export const UPDATE_SERVICEWORKER = "UPDATE_SERVICEWORKER";

export function updateServiceWorker() {
  return {
    type: UPDATE_SERVICEWORKER
  };
}

export const reducer = (
  state = {
    serviceWorkerUpdated: false
  },
  action
) => {
  switch (action.type) {
    case UPDATE_SERVICEWORKER: {
      return {
        ...state,
        serviceWorkerUpdated: true
      };
    }
    default:
      return state;
  }
};

If the UPDATE_SERVICEWORKER action is dispatched, the state of the app will be updated with serviceWorkerUpdated = true. Then, store configurations should be updated passing service-worker-reducer as the root reducer.

/* configure-store.js */

import { createStore } from "redux";
import { reducer as rootReducer } from "./service-worker-update";

const configureStore = (initialState = { serviceWorkerUpdated: false }) => {
  return createStore(rootReducer, initialState);
};

On-update callback

At the current state, we didn't dispatch any actions yet. The question, here, is: when the service worker update action should be dispatched? Fortunately, there is a service worker implementation shipped with Create React App that has initial control on service worker state changing. This implementation will be found in registerValidSW method in service worker file shipped with CRA. This method is invoked once you change serviceWorker.unregister() to serviceWorker.register(), remember?

This method takes the path of the service worker file in the project and config object which will be used later to carry an update callback function. You may ask what's the callback function? why I need it?, well, this callback function will dispatch the service worker update action, and it will be called when there is a service worker update installed and waiting to be activated. This can be found in the code snippet below imported from CRA serviceWorker.js.

/* from serviceWorker.js shipped with CRA */

registration.onupdatefound = () => {
  const installingWorker = registration.installing;
  if (installingWorker == null) {
    return;
  }
  installingWorker.onstatechange = () => {
    installingWorker.onstatechange = () => {
      if (installingWorker.state === "installed") {
        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 a ll " +
              "tabs for this page are closed. See https://bit .ly/CRA-PWA."
          );

          // Execute callback
          if (config && config.onUpdate) {
            config.onUpdate();
          }
        } else {
          // At this point, everything has been precached.
          // It's the perfect time to display a
          // "Content is cached for offline use." message.
          console.log("Content is cached for offline use.");

          // Execute callback
          if (config && config.onSuccess) {
            config.onSuccess(registration);
          }
        }
      }
    };
  };
};

You see, we can check on config object with onUpdate callback. Then we invoke it. But, what will this function do? it will dispatch the action, finally! :tada:

const onUpdate = () => {
  store.dispatch(updateServiceWorker());
};

After all of this, we can add the implementation of the callback function in index.js and supply a provider that will expose the service worker state in the root component. We do this by:

ā€¢ Wrapping the rendering root component with a Provider imported from react-redux passing the store as props. This will make the store available in all nested components. Intuitively, the store will be available in the whole app components with the connect() currying function.

/*from index.js*/

ReactDOM.render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById("root")
);

ā€¢ mapStateToProps

Once service worker state is exposed in the app, it will be needed to extract it in the root component. We do this by using mapStateToProps with connect in the root component. First we will connect the root component with mapStateToProp function:

export default connect(mapStateToProps)(App);

Then we will extract the service worker state as app prop:

const mapStateToProps = state => {
  return {
    serviceWorkerUpdated: state.serviceWorkerUpdated
  };
};

Everything tends to have an end when the rendering time comes. we need to push a notification every time the app finish with rendering all the components proberly and there is an update available. We can get this by using componentDidUpdate and componentDidMount or even useEffect react hook. In this article I will try useEffect but it's the same to do it with others.

Notification component

At this moment, our app has a prop called serviceWorkerUpdated. It's a boolean, we need to check if it is true in useEffect function. If this prop value is true then a push notification should be displayed else nothing happend

useEffect(() => {
  if (props.serviceWorkerUpdated) {
    toast(<Msg />, {
      position: "bottom-right"
    });
  }
}, [props.serviceWorkerUpdated]);

I used props.serviceWorkerUpdated dependency as a second argument to avoid firing this effect when the service worker state hasn't changed. Also, as you can see I am using react-toastify. I will not dig into any details here, you can find this in their documentation. The major thing is the message component that will be rendered to the user:

const Msg = ({ closeToast }) => (
  <>
    <p> Update available, Please refresh your browser!</p>
    <div>
      <span>From PCs: Press Ctrl + Shift + R </span>
      <span>From Mobile Phones: Close all your opened tabs</span>
    </div>
  </>
);

According to toastify docs this component will not work unless you define a container for it in the app:

/* toast container as child in your app */

<ToastContainer position="bottom-right" toastClassName="toast-container" />

Summary

We've created the redux store for the main app exposing the service worker state in our app, then dispatched an update action when there is "updates in the content" flag raised from service worker. Finally, we've extracted this state after the root component rendering finished. If there is any update notify the user else nothing happened.

Source code available on Github

ŁArticle Url: https://syncatallah.cc/writings/notify-pwa-updates

Did you like this article?

Ahmad Atallah

See other articles by Ahmad

Related jobs

See all

Title

The company

  • Remote

Title

The company

  • Remote

Title

The company

  • Remote

Title

The company

  • Remote

Related articles

JavaScript Functional Style Made Simple

JavaScript Functional Style Made Simple

Daniel Boros

ā€¢

12 Sep 2021

JavaScript Functional Style Made Simple

JavaScript Functional Style Made Simple

Daniel Boros

ā€¢

12 Sep 2021

WorksHub

CareersCompaniesSitemapFunctional WorksBlockchain WorksJavaScript WorksAI WorksGolang WorksJava WorksPython WorksRemote Works
hello@works-hub.com

Ground Floor, Verse Building, 18 Brunswick Place, London, N1 6DZ

108 E 16th Street, New York, NY 10003

Subscribe to our newsletter

Join over 111,000 others and get access to exclusive content, job opportunities and more!

Ā© 2024 WorksHub

Privacy PolicyDeveloped by WorksHub