How to add PWA support in React

Progressive Web Apps combine the best of web and native applications, providing offline functionality, push notifications, and home screen installation for enhanced user engagement. As the creator of CoreUI, a widely used open-source UI library, I’ve built PWA-enabled dashboards throughout my 12 years of frontend development since 2014. The most reliable approach is using Create React App’s built-in PWA template or adding Workbox for custom service worker configuration. This method provides automatic caching strategies, offline support, and update management without complex service worker code.

Create manifest.json and register service worker with Workbox for comprehensive PWA functionality.

// public/manifest.json
{
  "short_name": "MyApp",
  "name": "My Progressive Web Application",
  "description": "A powerful PWA built with React",
  "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": "#000000",
  "background_color": "#ffffff",
  "orientation": "portrait"
}
<!-- public/index.html -->
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <meta name="theme-color" content="#000000" />
    <meta name="description" content="My Progressive Web App" />
    <link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
    <link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
    <title>My PWA</title>
  </head>
  <body>
    <noscript>You need to enable JavaScript to run this app.</noscript>
    <div id="root"></div>
  </body>
</html>

Install Workbox and configure service worker:

npm install workbox-webpack-plugin
// src/service-worker.js
import { clientsClaim } from 'workbox-core'
import { ExpirationPlugin } from 'workbox-expiration'
import { precacheAndRoute, createHandlerBoundToURL } from 'workbox-precaching'
import { registerRoute } from 'workbox-routing'
import { StaleWhileRevalidate, CacheFirst } from 'workbox-strategies'

clientsClaim()

precacheAndRoute(self.__WB_MANIFEST)

const fileExtensionRegexp = new RegExp('/[^/?]+\\.[^/]+$')
registerRoute(
  ({ request, url }) => {
    if (request.mode !== 'navigate') {
      return false
    }
    if (url.pathname.startsWith('/_')) {
      return false
    }
    if (url.pathname.match(fileExtensionRegexp)) {
      return false
    }
    return true
  },
  createHandlerBoundToURL(process.env.PUBLIC_URL + '/index.html')
)

registerRoute(
  ({ url }) => url.origin === self.location.origin && url.pathname.endsWith('.png'),
  new CacheFirst({
    cacheName: 'images',
    plugins: [
      new ExpirationPlugin({
        maxEntries: 60,
        maxAgeSeconds: 30 * 24 * 60 * 60
      })
    ]
  })
)

registerRoute(
  ({ url }) => url.origin === 'https://api.example.com',
  new StaleWhileRevalidate({
    cacheName: 'api-cache',
    plugins: [
      new ExpirationPlugin({
        maxEntries: 50,
        maxAgeSeconds: 5 * 60
      })
    ]
  })
)

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

Register service worker in React application:

// src/serviceWorkerRegistration.js
export function register(config) {
  if ('serviceWorker' in navigator) {
    window.addEventListener('load', () => {
      const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`

      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) {
                  console.log('New content available, please refresh')
                  if (config && config.onUpdate) {
                    config.onUpdate(registration)
                  }
                } else {
                  console.log('Content cached for offline use')
                  if (config && config.onSuccess) {
                    config.onSuccess(registration)
                  }
                }
              }
            }
          }
        })
        .catch((error) => {
          console.error('Service worker registration failed:', error)
        })
    })
  }
}

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

Add install prompt handler:

import { useState, useEffect } from 'react'

const PWAInstallPrompt = () => {
  const [deferredPrompt, setDeferredPrompt] = useState(null)
  const [showInstall, setShowInstall] = useState(false)

  useEffect(() => {
    const handler = (e) => {
      e.preventDefault()
      setDeferredPrompt(e)
      setShowInstall(true)
    }

    window.addEventListener('beforeinstallprompt', handler)

    return () => window.removeEventListener('beforeinstallprompt', handler)
  }, [])

  const handleInstall = async () => {
    if (!deferredPrompt) {
      return
    }

    deferredPrompt.prompt()
    const { outcome } = await deferredPrompt.userChoice

    if (outcome === 'accepted') {
      console.log('User accepted install prompt')
    }

    setDeferredPrompt(null)
    setShowInstall(false)
  }

  if (!showInstall) {
    return null
  }

  return (
    <div style={{
      position: 'fixed',
      bottom: 20,
      right: 20,
      padding: '1rem',
      background: '#007bff',
      color: 'white',
      borderRadius: '8px',
      boxShadow: '0 4px 6px rgba(0,0,0,0.1)'
    }}>
      <p>Install our app for better experience!</p>
      <button onClick={handleInstall}>Install</button>
      <button onClick={() => setShowInstall(false)}>Dismiss</button>
    </div>
  )
}

Here the manifest.json defines PWA metadata including name, icons, display mode, and theme colors for native-like appearance. The service worker file uses Workbox for declarative caching strategies without manual cache management. The precacheAndRoute function automatically caches all built assets during installation. The CacheFirst strategy caches images for 30 days with maximum 60 entries for offline access. The StaleWhileRevalidate strategy serves cached API responses while fetching updates in background. The beforeinstallprompt event enables custom installation UI instead of browser default prompt. The registration checks for service worker updates and notifies users when new content becomes available.

Best Practice Note:

This is the PWA implementation we use in CoreUI Pro dashboards for offline-capable admin applications with automatic updates and installation prompts. Test PWA functionality using Chrome DevTools Lighthouse and Application panel to verify service worker registration and caching strategies, implement background sync for offline form submissions and data updates, and add push notification support for real-time user engagement when using Firebase Cloud Messaging or similar services.


Speed up your responsive apps and websites with fully-featured, ready-to-use open-source admin panel templates—free to use and built for efficiency.


About the Author