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.
Now let’s see step-by-step implementation on how to develop a Progressive Web Application using React.
Step 1: 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: 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 install –save web-push react-router-dom bootstrap react-bootstrap
Project Structure: Add worker.js and feed.js in your public folder and a components folder inside your src folder, so that it looks like this.
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
var STATIC_CACHE_NAME = "gfg-pwa" ; var DYNAMIC_CACHE_NAME = "dynamic-gfg-pwa" ; // Add Routes and pages using React Browser Router var 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) => { var 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: 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.
Javascript
<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: 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.
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.
Javascript
<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: 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
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.
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
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
window.onload = function () { var photo = document.getElementById( "photoBtn" ); var locationBtn = document.getElementById( "locationBtn" ); locationBtn.addEventListener( "click" , handler); var 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); var lat = position.coords.latitude; var 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) { var 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" ; var 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.
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.