'why is onupdatefound method not getting triggered in dockerised PWA react app

I am working on an dockerised app whose frontend is built with create-react-app.

Note that this app was NOT built using pwa template. i.e, using npx create-react-app --template cra-template-pwa hence this does not has workbox implementation.

Here is the folder structure of the project

--apps
|__api
|__frontend
  |__public
    |__index.html
    |__manifest.json
    |__service-worker.js
  |__src
    |__App.js
    |__index.js
    |__serviceWorkerRegistration.js

Here is the how files are public/index.html

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <meta name="theme-color" content="#fff">
    <meta name="apple-mobile-web-app-status-bar-style" content="#fff">
    <meta name="facebook-domain-verification" content="yb2nwega2uk10tkg60916ofwqu02xx">

   
    <link rel="manifest" href="%PUBLIC_URL%/manifest.json"> 
    <style>
      #snackbar {
        visibility: hidden;
        min-width: 250px;
        margin-left: -125px;
        background-color: #333;
        color: #fff;
        text-align: center;
        border-radius: 2px;
        padding: 16px;
        position: fixed;
        z-index: 1;
        left: 50%;
        bottom: 30px;
      }
  
      #snackbar.show {
        visibility: visible;
        -webkit-animation: fadein 0.5s;
        animation: fadein 0.5s;
      }
  
      @-webkit-keyframes fadein {
        from {
          bottom: 0;
          opacity: 0;
        }
        to {
          bottom: 30px;
          opacity: 1;
        }
      }
  
      @keyframes fadein {
        from {
          bottom: 0;
          opacity: 0;
        }
        to {
          bottom: 30px;
          opacity: 1;
        }
      }
    </style>
  </head>
  <body>
    <noscript>You need to enable JavaScript to run this app.</noscript>
    <div id="root"></div>
    <div id="snackbar">A new version of this app is available. Click <a id="reload">here</a> to update.</div>
  </body>
</html>

public/service-worker.js

/* eslint no-restricted-globals: 0 */
/* eslint array-callback-return: 0 */

const HTMLToCache = '/';
const version = 'MSW V0.3';

self.addEventListener('install', event => {
  event.waitUntil(
    caches.open(version).then(cache => {
      cache.add(HTMLToCache).then(self.skipWaiting());
    })
  );
});

self.addEventListener('message', event => {
  if (event.data && event.data.action === 'skipWaiting') {
    self.skipWaiting();
  }
});

self.addEventListener('activate', event => {
  event.waitUntil(
    caches
      .keys()
      .then(cacheNames =>
        Promise.all(
          cacheNames.map(cacheName => {
            if (version !== cacheName) return caches.delete(cacheName);
          })
        )
      )
      .then(self.clients.claim())
  );
});

self.addEventListener('fetch', event => {
  const requestToFetch = event.request.clone();
  event.respondWith(
    caches.match(event.request.clone()).then(cached => {
      // We don't return cached HTML (except if fetch failed)
      if (cached) {
        const resourceType = cached.headers.get('content-type');
        // We only return non css/js/html cached response e.g images
        if (!hasHash(event.request.url) && !/text\/html/.test(resourceType)) {
          return cached;
        }

        // If the CSS/JS didn't change since it's been cached, return the cached version
        if (
          hasHash(event.request.url) &&
          hasSameHash(event.request.url, cached.url)
        ) {
          return cached;
        }
      }
      return fetch(requestToFetch)
        .then(response => {
          const clonedResponse = response.clone();
          const contentType = clonedResponse.headers.get('content-type');

          if (
            !clonedResponse ||
            clonedResponse.status !== 200 ||
            clonedResponse.type !== 'basic' ||
            /\/sockjs\//.test(event.request.url)
          ) {
            return response;
          }

          if (/html/.test(contentType)) {
            caches
              .open(version)
              .then(cache => cache.put(HTMLToCache, clonedResponse));
          } else {
            // Delete old version of a file
            if (hasHash(event.request.url)) {
              caches.open(version).then(cache =>
                cache.keys().then(keys =>
                  keys.forEach(asset => {
                    if (
                      new RegExp(removeHash(event.request.url)).test(
                        removeHash(asset.url)
                      )
                    ) {
                      cache.delete(asset);
                    }
                  })
                )
              );
            }

            caches.open(version).then(cache => {
              if (event.request.url.indexOf('http') !== 0) return;
              cache.put(event.request, clonedResponse);
            });
          }
          return response;
        })
        .catch(() => {
          if (hasHash(event.request.url))
            return caches.match(event.request.url);
          // If the request URL hasn't been served from cache and isn't sockjs we suppose it's HTML
          else if (!/\/sockjs\//.test(event.request.url))
            return caches.match(HTMLToCache);
          // Only for sockjs
          return new Response('No connection to the server', {
            status: 503,
            statusText: 'No connection to the server',
            headers: new Headers({ 'Content-Type': 'text/plain' }),
          });
        });
    })
  );
});

function removeHash(element) {
  if (typeof element === 'string') return element.split('?hash=')[0];
}

function hasHash(element) {
  if (typeof element === 'string') return /\?hash=.*/.test(element);
}

function hasSameHash(firstUrl, secondUrl) {
  if (typeof firstUrl === 'string' && typeof secondUrl === 'string') {
    return /\?hash=(.*)/.exec(firstUrl)[1] === /\?hash=(.*)/.exec(secondUrl)[1];
  }
}

src/index.js

import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import * as serviceWorkerRegistration from './serviceWorkerRegistration';

import { BrowserRouter as Router } from 'react-router-dom';
import { Provider } from 'react-redux';
import { configureStore } from './store';
import { StylesProvider } from '@material-ui/styles';

import App from './App';

const store = configureStore();


const AppContainer = (
  <Provider store={store}>
    <StylesProvider injectFirst>
      <Router>
        <App />
      </Router>
    </StylesProvider>
  </Provider>
);

ReactDOM.render(AppContainer, document.getElementById('root'));

serviceWorkerRegistration.register();

src/serviceWorkerRegistration.js

// This optional code is used to register a service worker.
// register() is not called by default.

// This lets the app load faster on subsequent visits in production, and gives
// it offline capabilities. However, it also means that developers (and users)
// will only see deployed updates on subsequent visits to a page, after all the
// existing tabs open on the page have been closed, since previously cached
// resources are updated in the background.

// To learn more about the benefits of this model and instructions on how to
// opt-in, read https://cra.link/PWA

const isLocalhost = Boolean(
  window.location.hostname === 'localhost' ||
    // [::1] is the IPv6 localhost address.
    window.location.hostname === '[::1]' ||
    // 127.0.0.0/8 are considered localhost for IPv4.
    window.location.hostname.match(
      /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
    )
);

export function register(config) {
  if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
    // The URL constructor is available in all browsers that support SW.
    const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href);
    if (publicUrl.origin !== window.location.origin) {
      // Our service worker won't work if PUBLIC_URL is on a different origin
      // from what our page is served on. This might happen if a CDN is used to
      // serve assets; see https://github.com/facebook/create-react-app/issues/2374
      return;
    }

    window.addEventListener('load', () => {
      const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
      if (isLocalhost) {
        // This is running on localhost. Let's check if a service worker still exists or not.
        checkValidServiceWorker(swUrl, config);

        // Add some additional logging to localhost, pointing developers to the
        // service worker/PWA documentation.
        navigator.serviceWorker.ready.then(() => {
          console.log(
            'This web app is being served cache-first by a service ' +
              'worker. To learn more, visit https://cra.link/PWA'
          );
        });
      } else {
        // Is not localhost. Just register service worker
        registerValidSW(swUrl, config);
      }
    });
  }
}

function registerValidSW(swUrl, config) {
  let newWorker;

  function showUpdateBar() {
    let snackbar = document.getElementById('snackbar');
    snackbar.className = 'show';
  }

  // The click event on the pop up notification
  document.getElementById('reload').addEventListener('click', function(event) {
    newWorker.postMessage({ action: 'skipWaiting' });
  });

  if ('serviceWorker' in navigator) {
    let refreshing;
    navigator.serviceWorker.addEventListener('controllerchange', function() {
      if (refreshing) return;
      window.location.reload();
      refreshing = true;
    });
  }
  navigator.serviceWorker
    .register(swUrl)
    .then(registration => {
      registration.onupdatefound = () => {
        newWorker = registration.installing;
        console.log('update found');
        const installingWorker = registration.installing;
        if (installingWorker == null) {
          return;
        }
        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 all ' +
                  'tabs for this page are closed. See https://cra.link/PWA.'
              );
              showUpdateBar();

              // Execute callback
              if (config && config.onUpdate) {
                console.log('on update');
                config.onUpdate(registration);
              }
            } 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);
              }
            }
          }
        };
      };
    })
    .catch(error => {
      console.error('Error during service worker registration:', error);
    });
}

function checkValidServiceWorker(swUrl, config) {
  // Check if the service worker can be found. If it can't reload the page.
  fetch(swUrl, {
    headers: { 'Service-Worker': 'script' },
  })
    .then(response => {
      // Ensure service worker exists, and that we really are getting a JS file.
      const contentType = response.headers.get('content-type');
      if (
        response.status === 404 ||
        (contentType != null && contentType.indexOf('javascript') === -1)
      ) {
        // No service worker found. Probably a different app. Reload the page.
        navigator.serviceWorker.ready.then(registration => {
          registration.unregister().then(() => {
            window.location.reload();
          });
        });
      } else {
        // Service worker found. Proceed as normal.
        registerValidSW(swUrl, config);
      }
    })
    .catch(() => {
      console.log(
        'No internet connection found. App is running in offline mode.'
      );
    });
}

export function unregister() {
  if ('serviceWorker' in navigator) {
    navigator.serviceWorker.ready
      .then(registration => {
        registration.unregister();
      })
      .catch(error => {
        console.error(error.message);
      });
  }
}

I have implemented PWA but something seems wrong. There are two things that I want to improve in the PWA

  1. Show a banner that lets user update the app whenever a new feature / bugfix is added to app although I never see onupdatefound listener getting triggered.
  2. Improve overall performance because PWA seems to slow down the API services

I'm wondering where am I going to wrong. Any help would be highly appreciated

Note: Things I have tried:

  • creating a new react app using npx create-react-app --template cra-template-pwa, copying service-worker.js in src/service-worker.js and installing all workbox-* dependencies This approach does not work because the service-worker.js file is in src and not in public folder and hence I see

Site cannot be installed: no matching service worker detected. You may need to reload the page, or check that the service worker for the current page also controls the start URL from the manifest

Something is off with how things are being bundled using webpack in the project that was built using --template cra-template-pwa vs simple project



Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source