Transforming Your React App into a PWA: A Comprehensive Guide
Tags: tutorial, typescript, pwa, react
Published: 2023-04-11 05:48:29

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`](https://github.com/elegantapp/pwa-asset-generator#install)

```bash
npx pwa-asset-generator public/favicons/android-chrome-512x512.png public/pwa-assets -b "#2F3437"
```

_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

   ```json
   {
     "src": "public/pwa-assets/manifest-icon-192.maskable.png",
     "sizes": "192x192",
     "type": "image/png",
     "purpose": "any"
   }
   ```

   update it to for every URL in the icons array

   ```json
   {
    "src": "/pwa-assets/manifest-icon-192.maskable.png",
    "sizes": "192x192",
    "type": "image/png",
    "purpose": "any"
   },
   ```

_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

   ```html
   <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)"
   />
   ```

   to

   ```html
   <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)"
   />
   ```

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

```html
<link rel="manifest" href="%PUBLIC_URL%/site.webmanifest" />
```

_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](https://web.dev/add-manifest/)

**Note that the screenshots field is optional**

```json
{
  "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"
    }
  ]
}
```

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

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

- for typescript - [here](https://github.com/cra-template/pwa/blob/main/packages/cra-template-pwa-typescript/template/src/service-worker.ts)

- for javascript - [here](https://github.com/cra-template/pwa/blob/main/packages/cra-template-pwa/template/src/service-worker.js)

## Create a `serviceWorkerResgistration.ts` file in your `src` folder

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

- for typescript - [here](https://github.com/cra-template/pwa/blob/main/packages/cra-template-pwa-typescript/template/src/serviceWorkerRegistration.ts)

- for javascript - [here](https://github.com/cra-template/pwa/blob/main/packages/cra-template-pwa/template/src/serviceWorkerRegistration.js)

## In your `src/index.tsx` file


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

serviceWorkerRegistration.register();
```

## You can test your PWA using `ngrok`

build your react app

```bash
npm run build
```

serve that build using `serve`

```bash
serve -s build
```

Then in another terminal window

```bash
ngrok http 3000
```

## (Optional) add custom install prompt


_1._  As stated [here](https://web.dev/learn/pwa/installation-prompt/#:~:text=For%20sites%20that%20pass%20the,users%20to%20install%20your%20app.&text=Users%20may%20not%20be%20familiar%20with%20the%20PWA%20install%20process.)

> 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

```ts
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];
}
```

_4._ If you are using typescript here's the `BeforeInstallPromptEvent` type [credits](https://stackoverflow.com/a/51847335/14225169)

```ts
/**
 * 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>;
}
```

_5._ You can use it like so

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

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

  return (
    <>{isPromptVisible && <Button onClick={promptToInstall}>Install</Button>}</>
  );
};
```

## Attribution

Thank you [Pargat Dhanjal](https://www.linkedin.com/in/pargat-singh-dhanjal-h3y/) for the amazing cover image!