Skip to main content

Command Palette

Search for a command to run...

Progressive Web Apps with React: Complete Setup Guide for Developers

A step-by-step walkthrough for building a production-ready PWA in React using Vite, vite-plugin-pwa, and Workbox.

Updated
7 min read
Progressive Web Apps with React: Complete Setup Guide for Developers
M
Building scalable web solutions, clean code systems, and performance-driven digital experiences.

I keep seeing devs reach straight for create-react-app's old PWA template, not realizing it's been deprecated for years, and the recommended path now runs through Vite plus the VitePWA plugin instead. So, let's actually walk through the current setup properly, the way I'd set it up on a real client project in 2026, not the way a five-year-old tutorial still floating around the internet tells you to.

Step 1: Scaffold With Vite, Not CRA

npm create vite@latest my-pwa-app -- --template react-ts
cd my-pwa-app
npm install

Vite's dev server and build pipeline are faster and the PWA plugin ecosystem around it is genuinely better maintained at this point. If you're starting a fresh React PWA in 2026 and still reaching for CRA, that's worth reconsidering before you write a single line of app logic.

Step 2: Install And Configure vite-plugin-pwa

npm install vite-plugin-pwa -D
// vite.config.ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import { VitePWA } from 'vite-plugin-pwa';

export default defineConfig({
  plugins: [
    react(),
    VitePWA({
      registerType: 'autoUpdate',
      manifest: {
        name: 'My PWA App',
        short_name: 'PWAApp',
        theme_color: '#0f172a',
        icons: [
          { src: 'icon-192.png', sizes: '192x192', type: 'image/png' },
          { src: 'icon-512.png', sizes: '512x512', type: 'image/png', purpose: 'maskable' }
        ]
      },
      workbox: {
        runtimeCaching: [
          {
            urlPattern: /^https:\/\/api\.yourdomain\.com\/.*/i,
            handler: 'NetworkFirst',
            options: { cacheName: 'api-cache', expiration: { maxEntries: 50 } }
          }
        ]
      }
    })
  ]
});

This single config block handles service worker generation, manifest injection, and caching strategy without you hand-rolling any of it manually, which is honestly where most React PWA tutorials make you do unnecessary extra work.

Step 3: Register Update Prompts In Your React Component

Auto-update is convenient, but users should know when a new version is available rather than silently sitting on stale cached code.

import { useRegisterSW } from 'virtual:pwa-register/react';

function UpdatePrompt() {
  const {
    needRefresh: [needRefresh],
    updateServiceWorker,
  } = useRegisterSW();

  if (!needRefresh) return null;

  return (
    <div className="update-toast">
      <span>New version available.</span>
      <button onClick={() => updateServiceWorker(true)}>Refresh</button>
    </div>
  );
}

Drop this near the root of your app, and you've solved the "stuck on stale cache" problem that plagues a lot of poorly configured PWAs in production.

Step 4: Handle Offline State Gracefully In The UI

Don't just let API calls silently fail when offline. Detect it and adjust the UI.

import { useState, useEffect } from 'react';

function useOnlineStatus() {
  const [isOnline, setIsOnline] = useState(navigator.onLine);

  useEffect(() => {
    const goOnline = () => setIsOnline(true);
    const goOffline = () => setIsOnline(false);
    window.addEventListener('online', goOnline);
    window.addEventListener('offline', goOffline);
    return () => {
      window.removeEventListener('online', goOnline);
      window.removeEventListener('offline', goOffline);
    };
  }, []);

  return isOnline;
}

Use this hook anywhere you need to show a banner, disable a submit button, or fall back to cached data instead of throwing a generic network error at someone mid-checkout.

Step 5: Persist State With IndexedDB For Real Offline Functionality

LocalStorage is fine for small flags, but for anything resembling real offline data, cart contents, draft forms, queued actions, use IndexedDB through a wrapper like idb rather than fighting the raw API directly.

npm install idb
import { openDB } from 'idb';

const dbPromise = openDB('app-store', 1, {
  upgrade(db) {
    db.createObjectStore('cart');
  },
});

export async function saveCartItem(id: string, item: object) {
  const db = await dbPromise;
  await db.put('cart', item, id);
}

This is the part a lot of React PWA tutorials skip entirely, leaving developers to discover IndexedDB's quirks the hard way mid-project once a client asks why their cart disappears after closing the browser.

Step 6: Test Installability And Lighthouse Score Before Calling It Done

Run a production build, not dev mode, since service workers behave differently and Lighthouse PWA audits won't pass against a dev server setup.

npm run build
npm run preview

Open Chrome DevTools, run a Lighthouse PWA audit against the preview build, and fix anything flagged before considering the setup finished. Skipping this step is how PWAs ship to production technically "working" but failing installability checks on certain Android devices, which defeats half the point of building one in the first place.

Step 7: Build A Custom Install Prompt Instead Of Relying On The Browser Default

The default browser install banner is fine, but it's not great UX, and you have very little control over when it appears. Capture the beforeinstallprompt event yourself and trigger it at a moment that actually makes sense, after someone's added something to a cart, or completed a meaningful action, rather than the moment they land on your homepage and have given you zero reason to trust the install yet.

import { useState, useEffect } from 'react';

function useInstallPrompt() {
  const [deferredPrompt, setDeferredPrompt] = useState<any>(null);

  useEffect(() => {
    const handler = (e: Event) => {
      e.preventDefault();
      setDeferredPrompt(e);
    };
    window.addEventListener('beforeinstallprompt', handler);
    return () => window.removeEventListener('beforeinstallprompt', handler);
  }, []);

  const promptInstall = async () => {
    if (!deferredPrompt) return;
    deferredPrompt.prompt();
    const { outcome } = await deferredPrompt.userChoice;
    setDeferredPrompt(null);
    return outcome;
  };

  return { canInstall: !!deferredPrompt, promptInstall };
}

Wire this into a button that only shows up once canInstall is true, and you've got full control over the install moment instead of leaving it to whatever timing the browser decides on its own. Worth noting this event doesn't fire on iOS Safari the same way, so you'll need a separate "Add to Home Screen" instructional component for Apple users specifically, since their install flow still works differently underneath.

This kind of fine-grained control matters a lot more on real client projects than tutorials usually let on. Teams running progressive web app in Ludhiana builds for businesses with actual conversion goals tend to obsess over exactly this detail, because a poorly timed install prompt gets dismissed and rarely shown again by the browser, so you genuinely only get one good shot at it per user in most cases.

A Few Real-World Notes From Client Projects

On a recent retail client build, we ran into a subtle bug where the NetworkFirst caching strategy was serving stale pricing data for a few seconds after a backend update, which is exactly the kind of thing that looks fine in testing and causes a support ticket in production. Switching that specific endpoint to NetworkOnly while keeping NetworkFirst for less time-sensitive data fixed it. The lesson here: don't apply one caching strategy globally across your whole API surface, decide per-endpoint based on how stale data is allowed to be.

If you're building something closer to an ecommerce web app development in Ludhiana project specifically, pay extra attention to this caching granularity around pricing and inventory endpoints, since that's exactly where stale-cache bugs cause real financial confusion for customers, not just a minor visual glitch.

When You'd Still Want Native Alongside This Setup

This React PWA setup gets you a long way, but if your project genuinely needs deep iOS-specific capabilities down the line, certain push notification behaviors, specific hardware permissions, pairing this React PWA foundation with dedicated iOS app development services later is a reasonable path rather than trying to force everything through browser APIs that iOS still handles a bit differently than Android.

Closing Thoughts

That's the full setup, scaffold, configure, handle updates, manage offline state properly, and actually test before shipping. None of these steps are individually complicated, but skipping any one of them is exactly how React PWAs end up half-working in production. If you'd rather have an experienced team handle this end to end instead of debugging service worker caching at midnight, a web & app development company Ludhiana with React PWA experience can take this from scaffold to production without the trial-and-error part.

Go set this up on a side project this weekend if nothing else, it's the fastest way to actually internalize how the pieces fit together instead of just reading about it.