In this blog we will learn how to convert a React App into a PWA using Workbox WebpackPlugin, workbox-window etc. We will do pre-caching of a few assets and few api response at runtime, using strategies like StaleWhileRevalidate
and Cache First
etc.

Install workbox webpack plugin
npm i workbox-webpack-plugin copy-webpack-plugin -D
Install these packages.
npm i workbox-core workbox-expiration workbox-precaching workbox-routing workbox-strategies workbox-cacheable-response workbox-window
Webpack Configuration
Add these configuration to webpack:
// @see https://developers.google.com/web/tools/workbox/reference-docs/latest/module-workbox-webpack-plugin.InjectManifestconst WorkboxWebpackPlugin = require("workbox-webpack-plugin");
const CopyPlugin = require("copy-webpack-plugin");
const webpackPlugins = [
new CopyPlugin( {
patterns: [
{ from: './src/favicon.ico', to: '' },
{ from: './src/manifest.json', to: '' },
{ from: './src/logo192.png', to: '' },
{ from: './src/logo512.png', to: '' },
],
} ),
];
if ( 'production' === process.env.NODE_ENV ) {
webpackPlugins.push( new InjectManifest( {
swSrc: './src/src-sw.js',
swDest: 'sw.js',
} ) );
}
module.exports = {
plugins: webpackPlugins
}
Manifest file
Add the manifest file manifest.json
in `src`. Also make sure to add favicon.ico, logo192.png and logo512.png according to your own choice with given sizes.
{
"short_name": "React PWA",
"name": "React PWA Example",
"icons": [
{
"src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
},
{
"src": "logo192.png",
"type": "image/png",
"sizes": "192x192"
},
{
"src": "logo512.png",
"type": "image/png",
"sizes": "512x512"
}
],
"start_url": ".",
"display": "standalone",
"theme_color": "#e50914",
"background_color": "#221f1f"
}
Add your favicon and manifest link in your public/index.html file
<link rel="manifest" href="manifest.json">
<link rel="shortcut icon" href="favicon.ico">
Service Worker Registration
Create a file called serviceWorkerRegistration.js
import { Workbox } from 'workbox-window';
export default function registerServiceWorker() {
if ( 'production' !== process.env.NODE_ENV ) {
return;
}
// Check if the serviceWorker Object exists in the navigator object ( means if browser supports SW )
if ('serviceWorker' in navigator) {
const wb = new Workbox('sw.js');
wb.addEventListener('installed', event => {
/**
* We have the condition — event.isUpdate because we don’t want to show
* this message on the very first service worker installation,
* only on the updated
*/
if (event.isUpdate) {
if (confirm(`New app update is available!. Click OK to refresh`)) {
window.location.reload();
}
}
});
wb.register();
}
}
Adding Precaching and Routes in Service Worker file
Create a file called src-sw.js
in your src directory.
See https://developers.google.com/web/tools/workbox/modules
/* eslint-disable no-restricted-globals */
// This service worker can be customized!
// See https://developers.google.com/web/tools/workbox/modules
// for the list of available Workbox modules, or add any other
// code you'd like.
// You can also remove this file if you'd prefer not to use a
// service worker, and the Workbox build step will be skipped.
import { clientsClaim } from 'workbox-core';
import { ExpirationPlugin } from 'workbox-expiration';
import { precacheAndRoute } from 'workbox-precaching';
import { registerRoute } from 'workbox-routing';
import { StaleWhileRevalidate, CacheFirst } from 'workbox-strategies';
import {CacheableResponsePlugin} from 'workbox-cacheable-response';
clientsClaim();
/**
* We are not wrapping it in a 'message' event as per the new update.
* @see https://developers.google.com/web/tools/workbox/modules/workbox-core
*/
self.skipWaiting();
/**
* Precache all of the assets generated by your build process.
* Their URLs are injected into the manifest variable below.
* This variable must be present somewhere in your service worker file,
* even if you decide not to use precaching. See https://cra.link/PWA
*/
precacheAndRoute(self.__WB_MANIFEST);
// Cache the Google Fonts stylesheets with a stale-while-revalidate strategy.
// @see https://developers.google.com/web/tools/workbox/guides/common-recipes#google_fonts
registerRoute(
({url}) => url.origin === 'https://fonts.googleapis.com',
new StaleWhileRevalidate({
cacheName: 'google-fonts-stylesheets',
})
);
// Cache the underlying font files with a cache-first strategy for 1 year.
// @see https://developers.google.com/web/tools/workbox/guides/common-recipes#google_fonts
registerRoute(
({url}) => url.origin === 'https://fonts.gstatic.com',
new CacheFirst({
cacheName: 'google-fonts-webfonts',
plugins: [
new CacheableResponsePlugin({
statuses: [0, 200],
}),
new ExpirationPlugin({
maxAgeSeconds: 60 * 60 * 24 * 365,
maxEntries: 30,
}),
],
})
);
/**
* Move api.
*
* Caches at: runtime
*/
registerRoute(
({url}) => url.origin === 'https://api.themoviedb.org' &&
url.pathname.startsWith('/3/discover/tv'),
new StaleWhileRevalidate({
cacheName: 'movie-api-response',
plugins: [
new CacheableResponsePlugin({
statuses: [0, 200],
}),
new ExpirationPlugin({maxEntries: 1}), // Will cache maximum 1 requests.
]
})
);
/**
* We use CacheFirst for images because, images are not going to change very often,
* so it does not make sense to revalidate images on every request.
*
* @see https://developers.google.com/web/tools/workbox/guides/common-recipes#caching_images
*/
registerRoute(
({request}) => request.destination === 'image',
new CacheFirst({
cacheName: 'images',
plugins: [
new CacheableResponsePlugin({
statuses: [0, 200],
}),
new ExpirationPlugin({
maxEntries: 60,
maxAgeSeconds: 30 * 24 * 60 * 60, // 30 Days
}),
],
}),
);
// @see https://developers.google.com/web/tools/workbox/guides/common-recipes#cache_css_and_javascript_files
registerRoute(
({request}) => request.destination === 'script' ||
request.destination === 'style',
new StaleWhileRevalidate({
cacheName: 'static-resources',
})
);
Build and serve
Now run production build and serve and check it http://localhost:5000
npm run serve

This is how your dis folder would look like
Your manifest file






Now click on Install by clicking on the download icon in the address bar.

Your app is now visible on the desktop along with other apps.

Now you can see the PWA is open on the desktop

To uninstall the PWA, click on Uninstall app by clicking on the three dots on top right corner.

Testing Offline
Now click on offline

Now our app works offline including the runtime api response as its cached.

Notice the API response is from service worker.

Images and other assets are also being served with Service Worker when offline
