Progressive Web App (2) How to apply React PWA

Development team

10/24/2021

In Progressive Web App Part 1, we learned what PWAs and serviceworkers are, how they work, and what their advantages/disadvantages are. In Part 2, we’ll look at a real-world React PWA example and learn how to apply React PWAs.


Using the Create React App (CRA).

The easiest way to set up a React PWA is to use Create React App (CRA). You can use CRA’s custom template feature to launch a working app with a PWA already set up.

# JavaScript
npx create-react-app my-app --template cra-template-pwa

# TypeScript
npx create-react-app my-app --template cra-template-pwa-typescript

There are a few settings in this template, and Google’s Workbox is preset to behave like Webpack, allowing the serviceworker to precache the bundles that are generated when building a React app. In this post, we’ll analyze code written in TypeScript.


Building the project with the service-worker.ts file.

The service-worker.ts file is where the Service Worker is written, and a version transpiled to process.env.PUBLIC_URL/service-worker.js becomes accessible after the project is built. This file is written using Google’s Workbox, rather than using the standard API, so you can easily modify the file without having to understand the entire complex lifecycle of a traditional service worker. The most important parts of this file are the precacheAndRoute, registerRoute functions.

precacheAndRoute(self.__WB_MANIFEST);
...registerRoute(
      ({ request, url }: { request: Request; url: URL }) => {
        ... },
  createHandlerBoundToURL(process.env.PUBLIC_URL + '/index.html')
);

Using the precacheAndRoute and registerRoute functions.

The precacheAndRoute function is imported from the workbox-precaching package and handles caching the entries of the path given as parameters into a precache list, and responding to routing when it occurs for the cached path. The self.__WB_MANIFEST it contains doesn’t have any special value before build, it’s just a placeholder for Webpack to reference when creating the service-worker.js file. The CRA already has this set up, but here’s the code to do it manually for reference.

// webpack.config.js
const WorkboxPlugin = require('workbox-webpack-plugin');

module.exports = {
      ...
  plugins: [
        ...
    new WorkboxPlugin.InjectManifest({
          swSrc: './src/service-worker.ts',
      swDest: 'service-worker.js',
    }),
  ],
};

The registerRoute function takes a Regex, a string, or a function and a handler to set up caching in the desired way depending on the asset, path, or file. There are five possible caching strategies defined in the workbox-strategies plugin that can be set here:

  • Stale-While-Revalidate: If there is a cached response, it will be responded to directly, if not, a fallback to the network request will occur. After responding with the cached response, the network request will update the cache in the background. This is the most commonly used option by default.
  • Cache First: Often used for static assets that don’t need to be updated frequently, and unlike Stale-While-Revalidate, it will respond immediately and not update the cache if it is cached.
  • Network First: Performed for materials that are updated frequently and it is important to always get the latest data, it will prioritize getting the latest data from the network and if this fails, it will respond with cached data.
  • Network Only: Does not use caching at all and only uses network resources.
  • Cache Only: Rarely used, and sets responses to come from cache only. Only makes sense if precaching is done manually.


.

serviceWorkerRegistration.ts

You can register a service worker and handle the events behind it at the same time. For web apps with PWAs, we need to be careful about cache cycles. An easy mistake for developers of traditional React apps to make is that PWA means that if you have multiple apps open at the same time for the same site, refreshing in one tab will not load the new app.

This behavior is described in detail in the Offline-First Considerations section of the React PWA documentation in Create React App, where the Service worker only runs one version of the site at a time because the pages in scope must all be controlled by the same SW, so if the same website is running across multiple tabs or windows, refreshing in one tab won’t download a new webapp.

In this case, even if a new version of the Service Worker is loaded and installed, it will wait in a WAITING state for all web apps to exit, and the user will not be able to run the new version. This policy works well for ensuring consistency of apps across tabs, but in situations where a webapp has a bug and needs to be hotfixed quickly, this can lead to a serious problem where the existing buggy webapp continues to load even when the user takes the intuitive action of refreshing.

With PWAs, you can notify users of new versions and maintain version consistency across tabs.

https://cdn-api.elice.io/api-attachment/attachment/a56434d67814443d95c9a2c761df0dc1/image.png\” alt="How to apply React PWA">How to apply React PWA?


To fix this, you need to use the software that fixes the bug right after it is released,

  • discover that a new release has occurred
  • Notify users of this good news
  • At the same time, install the new SW in the background
  • activate the new SW
  • Refresh to load the webapp with the new SW

In total, five steps are required


Discover that a new release has occurred.

There are several ways to do this, you can periodically check for new versions of your service workers, or in an SPA like React, you can check for updates in a callback event that fires whenever a new location is updated in the history. In the code below, we manually run an update for (all) service workers registered in the browser when the user navigates to a new page in our React SPA.

history.listen((location, action) => {
      if (!navigator.serviceWorker) {
        return;
  }
  navigator.serviceWorker.getRegistrations().then(regs =>
    regs.forEach(reg => {
          reg.update().catch(e => {
            // Fetching SW failed.
      });
    })
  );
});

If a new service worker (new release) is found, that event can be handled in registration.onupdatefound inside serviceWorkerRegistration.ts.

function registerValidSW(swUrl: string, config?: Config) {
      navigator.serviceWorker
    .register(swUrl)
    .then(registration => {
          registration.onupdatefound = () => {
            ...
      }
  ...

Notify users.

The registration.installingWorker refers to the currently being/installed SW, and if its state is installed, it means that a new SW has been found, installed, and is waiting to be activated. The user can be notified with a popup. The following code is applied to Elice.

const installingWorker = registration.installing;
  if (installingWorker == null) {
        return;
  }
  installingWorker.onstatechange = () => {
        if (installingWorker.state === 'installed') {
          if (navigator.serviceWorker.controller) {
            doShowSWUpdateToast();
        ...
      }
    ...

Installation of new SW

If the user presses the refresh button in Toast as shown in the code above, we can force the newly installed service worker to be activated with the following code. The code below sends a SKIP_WAITING message only to the service workers currently installed in the browser whose status is waiting.

navigator.serviceWorker.getRegistrations().then(regs =>
  regs.forEach(reg => {
        reg?.waiting?.postMessage({ type: 'SKIP_WAITING' });
  })
);

refresh

Finally, we’ll notify the user that the new service worker has been installed, proceed to refresh, and the user will be able to use the new version of the webapp.


Application to an existing React App.

All of the above is based on the assumption that you are building a new React App with CRA. If you have an existing React App, adapting this is very easy.

  • Copy your service-worker.ts file
  • Copy the serviceWorkerRegistration.ts file
  • Add serviceWorkerRegistration.register() syntax to index.tsx
  • Proceed if Webpack setup is required, not required if CRA-based

With these relatively easy steps, you should be able to set up your PWA. In the next and final post, we will discuss the caching policies we applied to the different routes.


*This content is a work protected by copyright law and is copyrighted by Elice.
*The content is protected by copyright law and is copyrighted by Elice.

  • #PWA
  • #JavaScript

Bring innovative DX solutions to your organization

Sign up for a free trial and a business developer will provide you with a personalized DX solution consultation tailored to your business