Open In App

How to develop a Progressive Web App using ReactJS ?

Last Updated : 16 Nov, 2023
Improve
Improve
Like Article
Like
Save
Share
Report

Progressive React Applications respond very fast to user actions. They load fast and are engaging just like a mobile app. They can access Mobile device features, leverage the Operating System, and have a very high reach. It enables Installability, Background Syncing, Caching and Offline Support, and other features like Push Notifications. With React, we can very easily enhance web apps progressively to look and feel like native mobile applications.

Prerequisites:

Now let’s see step-by-step implementation on how to develop a Progressive Web Application using React.

Steps to develop a Progressive Web App using ReactJS

Step 1: Initialize React Project with pwa

With ReactJS, it is even easier to create a Progressive Web App or even convert an existing React project into one. In the terminal of your text editor, enter the following command. CRA creates a boilerplate for Progressive Web App that you can easily modify according to your needs.

npx create-react-app react-pwa --template cra-template-pwa
cd react-pwa

This command creates a new React Application named react-pwa and navigates to the directory of your app. You can further modify your manifest.json file and other files like the logo to customize the app and make it your own.

Step 2: Install Required Modules

Let’s implement the functionalities of a PWA and add more features to our App. In the terminal of your text editor enter the following command to install some third-party and npm packages.

npm i --save web-push react-router-dom bootstrap react-bootstrap

Add worker.js and feed.js in your public folder and a components folder inside your src folder,

Project Structure:

Project Structure

The updated dependencies list in package.json file.

"dependencies": {
"@testing-library/jest-dom": "^5.17.0",
"@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^13.5.0",
"bootstrap": "^5.3.2",
"react": "^18.2.0",
"react-bootstrap": "^2.9.1",
"react-dom": "^18.2.0",
"react-router-dom": "^6.18.0",
"react-scripts": "5.0.1",
"web-push": "^3.6.6",
"web-vitals": "^2.1.4",
"workbox-background-sync": "^6.6.0",
"workbox-broadcast-update": "^6.6.0",
"workbox-cacheable-response": "^6.6.0",
"workbox-core": "^6.6.0",
"workbox-expiration": "^6.6.0",
"workbox-google-analytics": "^6.6.0",
"workbox-navigation-preload": "^6.6.0",
"workbox-precaching": "^6.6.0",
"workbox-range-requests": "^6.6.0",
"workbox-routing": "^6.6.0",
"workbox-strategies": "^6.6.0",
"workbox-streams": "^6.6.0"
},

Step 3: Register a Service Worker

A Service Worker is a special kind of script file that works between the browser and the network. It helps us to perform unique functionalities and registers itself whenever a page is loaded. To register a new service worker in your React App, in the worker.js file in your public folder (public/worker.js), add the following code.

Javascript




// Filename - public/worker.js
 
let STATIC_CACHE_NAME = "gfg-pwa";
let DYNAMIC_CACHE_NAME = "dynamic-gfg-pwa";
 
// Add Routes and pages using React Browser Router
let urlsToCache = ["/", "/search", "/aboutus", "/profile"];
 
// Install a service worker
self.addEventListener("install", (event) => {
    // Perform install steps
    event.waitUntil(
        caches
            .open(STATIC_CACHE_NAME)
            .then(function (cache) {
                console.log("Opened cache");
                return cache.addAll(urlsToCache);
            })
    );
});
 
// Cache and return requests
self.addEventListener("fetch", (event) => {
    event.respondWith(
        caches.match(event.request).then((cacheRes) => {
            // If the file is not present in STATIC_CACHE,
            // it will be searched in DYNAMIC_CACHE
            return (
                cacheRes ||
                fetch(event.request).then((fetchRes) => {
                    return caches
                        .open(DYNAMIC_CACHE_NAME)
                        .then((cache) => {
                            cache.put(
                                event.request.url,
                                fetchRes.clone()
                            );
                            return fetchRes;
                        });
                })
            );
        })
    );
});
 
// Update a service worker
self.addEventListener("activate", (event) => {
    let cacheWhitelist = ["gfg-pwa"];
    event.waitUntil(
        caches.keys().then((cacheNames) => {
            return Promise.all(
                cacheNames.map((cacheName) => {
                    if (
                        cacheWhitelist.indexOf(
                            cacheName
                        ) === -1
                    ) {
                        return caches.delete(cacheName);
                    }
                })
            );
        })
    );
});


Step 4: Checking Service Worker support for browser

Some old browsers may not support service workers. However, most modern browsers like Google Chrome have in-built support for service workers. In case of the absence of support, the app will run like a normal web application. To make sure that we don’t run into an error or the app doesn’t crash, we need to check whether the support status of service workers in the browser of the client. To do so, update your index.html file in the public folder (public/index.html) with the following code.

HTML




<!-- Filename - public/index.html -->
<!-- Other HTML Code -->
<script>
    if ("serviceWorker" in navigator) {
 
        window.addEventListener("load", function () {
            navigator.serviceWorker
                .register("/worker.js")
                .then(
                    function (registration) {
                        console.log(
                            "Worker registration successful",
                            registration.scope);
                    },
                    function (err) {
                        console.log("Worker registration failed", err);
                    }
                )
                .catch(function (err) {
                    console.log(err);
                });
        });
    } else {
        console.log("Service Worker is not"
            + " supported by browser.");
    }
</script>


Step 5: Add Step to Register theService Worker

Now, that we have the code for the basic functionalities of a service worker. We need to register it. To do so, change one line in index.js in the src folder (src/index.js) from

service-worker.unregister()
to
serviceWorker.register()

Our service worker i.e. worker.js will now successfully register itself.

Step to run the application: Now, enter the following command in the terminal of your text editor.

npm start

Output: This will open your React App in the localhost://3000 in the browser. And, in ṯhe Dev Tools, under Application Tab, you can see that your service worker is registered with a “Worker registration successful” message in your console.

Service Worker Registered

Explanation: We now have the basic service worker functioning just the way we want it to. To implement other native device-like features, let us implement to send a notification in case the user goes offline while using the app. Also, to see your new features, there is no need to run the app again, just clicking on reload button will do the trick.

Step 6: Sending a Push Notification when Offline

Push Notifications are a native mobile feature. And the browser will automatically ask for user permission in default settings. Web-push is a third-party package that will aid us with VAPID keys to push a notification. Now, we need to have a VAPID API Key to start implementing Push notifications in our App. Note that every VAPID API KEY is unique for every service worker.

To generate the API Keys, type the following in the terminal:

./node_modules/.bin/web-push generate-vapid-keys

Now, in the terminal of your text editor, web-push provides two of your own vapid keys. We are going to use the public vapid key to generate push notifications.

Modify the script in index.html. This will encode your base64 string VAPID API KEY and connect it with the service worker so that it is able to send notifications.

HTML




<!-- Filename - public/index.html -->
<!-- Other HTML Code -->
 
<script>
    if ("serviceWorker" in navigator) {
        function urlBase64ToUint8Array(base64String) {
            const padding = "=".repeat((4 -
                (base64String.length % 4)) % 4);
            const base64 = (base64String + padding)
                .replace(/\-/g, "+")
                .replace(/_/g, "/");
 
            const rawData = window.atob(base64);
            const outputArray = new Uint8Array(rawData.length);
 
            for (let i = 0; i < rawData.length; ++i) {
                outputArray[i] = rawData.charCodeAt(i);
            }
            return outputArray;
        }
 
        function determineAppServerKey() {
            var vapidPublicKey =
                "YOUR_PUBLIC_KEY";
            return urlBase64ToUint8Array(vapidPublicKey);
        }
 
        window.addEventListener("load", function () {
            navigator.serviceWorker
                .register("/worker.js")
                .then(
                    function (registration) {
                        console.log(
                            "Worker registration successful",
                            registration.scope
                        );
                        return registration.pushManager
                            .getSubscription()
                            .then(function (subscription) {
                                registration.pushManager.subscribe({
                                    userVisibleOnly: true,
                                    applicationServerKey: determineAppServerKey(),
                                });
                            });
                    },
                    function (err) {
                        console.log("Worker registration failed", err);
                    }
                )
                .catch(function (err) {
                    console.log(err);
                });
        });
    } else {
        console.log("Service Worker is not supported by browser.");
    }
</script>


Step 7: Implement Push Notification Functionality

Let’s use this new functionality to send push notifications whenever we go offline. In worker.js modify the fetch event and add the following code. Inside the show notification function, you can add more properties and modify them according to your wishes.

Javascript




// Filename public/Worker.js
 
self.addEventListener("fetch", (event) => {
    event.respondWith(
        caches.match(event.request).then((cacheRes) => {
            return (
                cacheRes ||
                fetch(event.request).then((fetchRes) => {
                    return caches
                        .open(DYNAMIC_CACHE_NAME)
                        .then((cache) => {
                            cache.put(
                                event.request.url,
                                fetchRes.clone()
                            );
                            return fetchRes;
                        });
                })
            );
        })
    );
    if (!navigator.onLine) {
        if (
            event.request.url ===
        ) {
            event.waitUntil(
                self.registration.showNotification(
                    "Internet",
                    {
                        body: "internet not working",
                        icon: "logo.png",
                    }
                )
            );
        }
    }
});


The self.registration.showNotification function shows the desired notification and even asks for permission before showing one.

Step 8: To check that Syncing and Caching work when offline, you can change the status above your Service Worker in Dev Tools to ‘offline’ or do the same above the app. Now, whenever you go offline, you will see a push notification indicating that you went offline. 

Push Notification: Internet Not Working

Note that you are still able to see the pages though some functionalities might be lost. This is because these default pages and URLs once visited get stored in the cache. So, make sure to unregister and register it again under the Application Tab every time you make changes in your files during development.

Step 9: Adding Native Features like Camera and Geolocation – PWA enables using native features like accessing the webcam and figuring out location with the help of service workers. Let’s start with creating the UI for this first where we can use these functionalities, create a Profile.js file in ‘src/Profile.js’ which we can navigate to via /profile using React Routes.

Javascript




// Filename - src/Profile.js
 
import React from "react";
import {
    InputGroup,
    Button,
    Container,
} from "react-bootstrap";
 
const Profile = () => {
    return (
        <>
            <Container className={style.styling}>
                <br></br>
                <InputGroup className="mb-3">
                    <InputGroup.Text>
                        Latitude
                    </InputGroup.Text>
                    <InputGroup.Text id="latitude">
                        00
                    </InputGroup.Text>
                </InputGroup>
                <br></br>
                <InputGroup className="mb-3">
                    <InputGroup.Text>
                        Longitude
                    </InputGroup.Text>
                    <InputGroup.Text id="longitude">
                        00
                    </InputGroup.Text>
                </InputGroup>
                <br></br>
                <InputGroup className="mb-3">
                    <InputGroup.Text>
                        Location
                    </InputGroup.Text>
                </InputGroup>
                <Button
                    variant="outline-secondary"
                    id="locationBtn"
                >
                    Get Location
                </Button>
                <br></br>
                <br></br>
                <Button
                    variant="outline-secondary"
                    id="photoBtn"
                >
                    Take a Picture Now!
                </Button>
                <video
                    id="player"
                    autoPlay
                    width="320px"
                    height="240px"
                ></video>
                <canvas
                    id="canvas"
                    width="320px"
                    height="240px"
                    style={{ display: "none" }}
                ></canvas>
                <Button
                    variant="outline-secondary"
                    id="capture"
                >
                    Capture
                </Button>
                <br></br>
                <div id="pick-image">
                    <h6>Pick an Image instead</h6>
                    <input
                        type="file"
                        accept="image/*"
                        id="image-picker"
                    ></input>
                </div>
                <br></br>
                <br></br>
            </Container>
        </>
    );
};
 
export default Profile;


Step 10: Now let’s add a feed.js file in public/feed.js to implement the functionality of location and camera.

Javascript




// Filename - public/feed.js
 
window.onload = function () {
    let photo = document.getElementById("photoBtn");
    let locationBtn =
        document.getElementById("locationBtn");
    locationBtn.addEventListener("click", handler);
    let capture = document.getElementById("capture");
    photo.addEventListener("click", initializeMedia);
    capture.addEventListener("click", takepic);
};
 
function initializeLocation() {
    if (!("geolocation" in navigator)) {
        locationBtn.style.display = "none";
    }
}
 
function handler(event) {
    if (!("geolocation" in navigator)) {
        return;
    }
 
    navigator.geolocation.getCurrentPosition(function (
        position
    ) {
        console.log(position);
        let lat = position.coords.latitude;
        let lon = position.coords.longitude;
        console.log(lat);
        console.log(lon);
        latitude.innerHTML = lat;
        longitude.innerHTML = lon;
    });
}
 
function initializeMedia() {
    if (!("mediaDevices" in navigator)) {
        navigator.mediaDevices = {};
    }
 
    if (!("getUserMedia" in navigator.mediaDevices)) {
        navigator.mediaDevices.getUserMedia = function (
            constraints
        ) {
            let getUserMedia =
                navigator.webkitGetUserMedia ||
                navigator.mozGetUserMedia;
 
            if (!getUserMedia) {
                return Promise.reject(
                    new Error(
                        "getUserMedia is not implemented!"
                    )
                );
            }
 
            return new Promise(function (resolve, reject) {
                getUserMedia.call(
                    navigator,
                    constraints,
                    resolve,
                    reject
                );
            });
        };
    }
 
    navigator.mediaDevices
        .getUserMedia({ video: true })
        .then(function (stream) {
            player.srcObject = stream;
            player.style.display = "block";
        })
        .catch(function (err) {
            console.log(err);
            imagePicker.style.display = "block";
        });
}
 
function takepic(event) {
    canvas.style.display = "block";
    player.style.display = "none";
    capture.style.display = "none";
 
    let context = canvas.getContext("2d");
    context.drawImage(
        player,
        0,
        0,
        canvas.width,
        player.videoHeight /
            (player.videoWidth / canvas.width)
    );
    player.srcObject
        .getVideoTracks()
        .forEach(function (track) {
            track.stop();
        });
}


Step 11: Create a new file called feed.js in the (/src/public) folder. In feed.js, we use geolocation and mediaDevices to implement functionalities of location and camera respectively. You can also use the Google Geocoder API to convert these latitudes and longitudes into the name of a place.

Output: You can now navigate to localhost:3000/profile to take your picture and get the location.

Native Features Enabled

Explanation: Clicking on the Get Location button will trigger the navigator.geolocation.getCurrentPosition inside the handler function thereby populating the latitude and longitude fields with appropriate values. To get the exact name of the city, try using the Geocoder API as mentioned above. Similarly, clicking on the Take a Picture, Now Button will trigger the navigator.mediaDevices.getUserMedia inside the initializeMedia function thereby opening the front camera and taking a picture. Both these functions will first add for permissions and then execute themselves.



Like Article
Suggest improvement
Previous
Next
Share your thoughts in the comments

Similar Reads