DEV Community

Cover image for Transforming Your React App into a PWA: A Comprehensive Guide
Hussain Pettiwala
Hussain Pettiwala

Posted on

Transforming Your React App into a PWA: A Comprehensive Guide

Progressive Web Apps (PWAs) are web applications that are designed to provide a native app-like experience to users, including features such as offline accessibility, push notifications, and home screen installation. By implementing PWAs, you can improve user engagement, increase conversions, and deliver a more seamless experience for your users.

In this comprehensive guide, we'll walk through the steps to transform your react app (built using create-react-app) into a PWA. We'll cover everything from generating assets for your PWA, updating your web manifest, creating a service worker, and registering your service worker. We'll also see how to test your PWA using ngrok.

By the end of this guide, you will have converted your react app to a PWA, and that can be easily installed on their home screens for easy access. So let's get started!

Generate Assets for your PWA

1. We will use pwa-asset-generator

npx pwa-asset-generator public/favicons/android-chrome-512x512.png public/pwa-assets -b "#2F3437"
Enter fullscreen mode Exit fullscreen mode

2. CRA by default creates a site.webmanifest file update its contents by.

  1. copy icons content for your manifest.json file.
  2. here make sure that you remove the public prepend from the urls

For example:
your output might have been something like

   {
     "src": "public/pwa-assets/manifest-icon-192.maskable.png",
     "sizes": "192x192",
     "type": "image/png",
     "purpose": "any"
   }
Enter fullscreen mode Exit fullscreen mode

update it to for every URL in the icons array

   {
    "src": "/pwa-assets/manifest-icon-192.maskable.png",
    "sizes": "192x192",
    "type": "image/png",
    "purpose": "any"
   },
Enter fullscreen mode Exit fullscreen mode

3. Incase site.webmanifest does not exist in your project create one in your public folder and link it in your index.html file

  1. Here I would highly recommend to prepone the URLs with %PUBLIC_URL%/

For example:
Update

   <link
     rel="apple-touch-startup-image"
     href="public/pwa-assets/apple-splash-2048-2732.jpg"
     media="(device-width: 1024px) and (device-height: 1366px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)"
   />
Enter fullscreen mode Exit fullscreen mode

to

   <link
     rel="apple-touch-startup-image"
     href="%PUBLIC_URL%/public/pwa-assets/apple-splash-2048-2732.jpg"
     media="(device-width: 1024px) and (device-height: 1366px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)"
   />
Enter fullscreen mode Exit fullscreen mode

Note: you can do this quickly by using the multicursor feature of your code editor.

<link rel="manifest" href="%PUBLIC_URL%/site.webmanifest" />
Enter fullscreen mode Exit fullscreen mode

4. copy iOS meta tags content for your index.html file

Update webmanifest

This is an example webmanifest. You can get more details regarding the same here

Note that the screenshots field is optional

{
  "short_name": "App Name",
  "name": "App Name: What it does",
  "scope": "/",
  "id": "/?source=pwa",
  "start_url": "/?source=pwa",
  "display": "standalone",
  "theme_color": "#b241ff",
  "background_color": "#2F3437",
  "description": "Description of your app",
  "icons": [
    {
      "src": "public/pwa-assets/manifest-icon-192.maskable.png",
      "sizes": "192x192",
      "type": "image/png",
      "purpose": "any"
    },
    {
      "src": "public/pwa-assets/manifest-icon-192.maskable.png",
      "sizes": "192x192",
      "type": "image/png",
      "purpose": "maskable"
    },
    {
      "src": "public/pwa-assets/manifest-icon-512.maskable.png",
      "sizes": "512x512",
      "type": "image/png",
      "purpose": "any"
    },
    {
      "src": "public/pwa-assets/manifest-icon-512.maskable.png",
      "sizes": "512x512",
      "type": "image/png",
      "purpose": "maskable"
    }
  ],
  "screenshots": [
    {
      "src": "/screenshots/dark-1.png",
      "type": "image/png",
      "sizes": "417x720"
    },
    {
      "src": "/screenshots/home-screen-dark-1.png",
      "type": "image/png",
      "sizes": "417x720"
    },
    {
      "src": "/screenshots/light-1.png",
      "type": "image/png",
      "sizes": "417x720"
    },
    {
      "src": "/screenshots/home-screen-light-1.png",
      "type": "image/png",
      "sizes": "417x720"
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

Create a service-worker.ts file in your src folder

copy contents from the official create-react-app pwa template

  • for typescript - here

  • for javascript - here

Create a serviceWorkerResgistration.ts file in your src folder

copy contents from the official create-react-app pwa template

  • for typescript - here

  • for javascript - here

In your src/index.tsx file

...
import * as serviceWorkerRegistration from "./serviceWorkerResgistration";
...
...

serviceWorkerRegistration.register();
Enter fullscreen mode Exit fullscreen mode

You can test your PWA using ngrok

build your react app

npm run build
Enter fullscreen mode Exit fullscreen mode

serve that build using serve

serve -s build
Enter fullscreen mode Exit fullscreen mode

Then in another terminal window

ngrok http 3000
Enter fullscreen mode Exit fullscreen mode

(Optional) add custom install prompt

1. As stated here

Users may not be familiar with the PWA install process. As the developer, you will understand when it is the right time to invite the user to install the app.

2. You can add a custom install button that will invite the user to install the app.

3. create a useAddToHomescreenPrompt hook
credits : https://gist.github.com/rikukissa/cb291a4a82caa670d2e0547c520eae53

import { useEffect, useState } from "react";

export function useAddToHomescreenPrompt(): [
  BeforeInstallPromptEvent | null,
  () => void
] {
  const [prompt, setState] = useState<BeforeInstallPromptEvent | null>(null);

  const promptToInstall = () => {
    if (prompt) {
      return prompt.prompt();
    }
    return Promise.reject(
      new Error(
        'Tried installing before browser sent "beforeinstallprompt" event'
      )
    );
  };

  useEffect(() => {
    const ready = (e: BeforeInstallPromptEvent) => {
      e.preventDefault();
      setState(e);
    };

    window.addEventListener("beforeinstallprompt", ready as any);

    return () => {
      window.removeEventListener("beforeinstallprompt", ready as any);
    };
  }, []);

  return [prompt, promptToInstall];
}
Enter fullscreen mode Exit fullscreen mode

4. If you are using typescript here's the BeforeInstallPromptEvent type credits

/**
 * The BeforeInstallPromptEvent is fired at the Window.onbeforeinstallprompt handler
 * before a user is prompted to "install" a web site to a home screen on mobile.
 *
 */
interface BeforeInstallPromptEvent extends Event {
  /**
   * Returns an array of DOMString items containing the platforms on which the event was dispatched.
   * This is provided for user agents that want to present a choice of versions to the user such as,
   * for example, "web" or "play" which would allow the user to chose between a web version or
   * an Android version.
   */
  readonly platforms: Array<string>;

  /**
   * Returns a Promise that resolves to a DOMString containing either "accepted" or "dismissed".
   */
  readonly userChoice: Promise<{
    outcome: "accepted" | "dismissed";
    platform: string;
  }>;

  /**
   * Allows a developer to show the install prompt at a time of their own choosing.
   * This method returns a Promise.
   */
  prompt(): Promise<void>;
}
Enter fullscreen mode Exit fullscreen mode

5. You can use it like so

const IntallPwaComponent = () => {
  const [prompt, promptToInstall] = useAddToHomescreenPrompt();
  const [isPromptVisible, setIsPromptVisible] = useState(false);

  useEffect(() => {
    if (prompt) {
      setIsPromptVisible(true);
    }
  }, [prompt]);

  return (
    <>{isPromptVisible && <Button onClick={promptToInstall}>Install</Button>}</>
  );
};
Enter fullscreen mode Exit fullscreen mode

Attribution

Thank you Pargat Dhanjal for the amazing cover image!

Top comments (0)