Progressive web apps with create-react-app. And my experience.
If you start learning react now (mid 2018), there's a good chance that you'll start with the famous tool create-react-app
, the tool became a de-factor standard for creating react application without configuring any (or less) tools. I was always skeptical about anything and everything that compiles into JavaScript, but after working for about a couple of years with tools like Babel, I started loving some of them.
I was tasked with creating a Progressive Web APP at my job. Out CTO is a cool guy, he doesn't care what framework I use or what R&D I put in my work. As long as the delivery meets the timeline, and the application works great, he doesn't have any problem with any technology. With that being said, we all like react in our organisation. So I decided to do it with react and chose to use create-react-app
.
Let's talk about Progressive Web Apps for a while:
Being one of the biggest internet giant, Google has a lot of influence on the future of the web. They are good at it too. The way chrome is pushing the web forward is really worth the appreciation. Like google, we will call them PWA for the rest of this article.
Google is very excited about PWA, so am I. Every year, there are lots of discussions about PWA in google IO. You can check their YouTube Channel. for those cool articles.
In short, PWAs are specially created web application (websites) that can be installed like an application in supported devices (Android, iOS or Chrome OS even supports other chrome platforms as well like Desktop Chrome)
With each year's iteration, PWA gets more and more close to native applications, and the current implementation is very promising. I better not get into details abut PWA, The official PWA page has a lot of information. But in short, PWA has some mandatory components and features:
- The Application has a Valid Manifest
- The Application installs a service worker, that handles how the app will handle in offline. Basically getting rid of the dinosaur game.
- Is served from a secure origin (HTTPS), and doesn't have mixed content. (e.g: any linked file from insecure origin)
Here comes the good part, when you run npm build
on a create-react-app
application. Guess what, in production application, it creates a manifest
, and also utilizes a Service worker to serve the application cache-first
.
Allthough it was almost there... I had to do two small changes to turn it into full fledge Progressive web app.
First Step: Editing the manifest
The default manifest can be found in: public/manifest.json
and is almost 99% complete. It needs some minot adjustments. The default manifest looks something like this:
{
"short_name": "React App",
"name": "Create React App Sample",
"icons": [
{
"src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
}
],
"start_url": "./index.html",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
}
There are some obvious changes to be made, like the name
and short_name
values. Then comes the most important part: The icon. PWA requires a mandatory 192x192
icon and a 512x512
icon is highly recommended. So I copied the icon files in the public
folder and added the records in the manifest:
"icons": [
{
"src": "icon.png",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/png"
},
{
"src": "icon_512.png",
"sizes": "512x512",
"type": "image/png"
},
{
"src": "icon_192.png",
"sizes": "192x192",
"type": "image/png"
}
],
Neat. I also changed the theme_color
and background_color
variables to match the color scheme of my application. If you application is being served from a static file server, leave the start_url
the same. Or in my case, I had to make a minor change there:
"start_url": "/"
the already available "display":"standalone"
makes it load without any address bar or browser UI (Like a 'standalone' APP) and is a vital part of the PWA ecosystem. For more content rich apps and games, the "display":"fullscreen"
value can also be used (still not supported by safari at the time of writing this post).
To know more about Manifest, Click here.
Second Step: Enabling the service worker
Every PWA requires a working and registered service worker. If you go to the src/index.js
file, you should find something like this (the latest version during writing this, had this):
You just need to replace the unregister()
to register. So that the source code looks like this:
... And you are done for the most part.
Controlling the install flow:
If you build your application after the above manifest change, upload it in a https origin and navigate to it using latest Chrome for android or similar modern browser, You should see a Add To HomeScreen bar at the bottom of it. Many modern browser should automatically show it. Your users can click it and install the application. But let's see if we can improve this.
Oddly enough, The people behind Progressive web apps thought they should restrict the developer from giving total control over letting their users to install PWA. In order to let the user install it, The application needs to be Eligible first in the user's browser. Once the browser determines that the user is actually interested in the application, Only then the developer(application) gets the chance to show install prompt.
I really wonder why didn't they do the same with Push notification API, or Geolocation API. I'm super tired of the push notification prompt showing in roughly every one out of three sites I visit.
Back to topic, In order to know when the browser declares the eligibility, the application needs to listen for the event beforeinstallprompt
, once the browser fires that event, The user can store the event object, and then whenever the user needs to show the install prompt, the application should call prompt()
on the event object. But there's a catch. prompt()
can be only called once on an event reference. If the user denies installation, The application will have to keep listening for the beforeinstallprompt
event, and this time it will obviously invoke much later. Only then the user can call prompt()
on the new event again. All these to make sure the user is annoyed. I wish public services in our country were as careful.
There's another catch. You cannot just automatically call prompt()
that won't work either, it has to be called from a user generated event.
The way most of the developers do it is, when the event beforeinstallprompt
is invoked, they store a reference to the event in a public or easily accessible scope, and then shows a button. Once the button is clicked, inside the click
event handler of the button, The prompt()
function is called on the event reference.
In my case I used a small react Component I made : react-simple-conditional to render JSX based on a condition to render the button based on a boolean state
value.
Listened for the beforeinstallprompt
event inside the componentDidMount()
event on the App
Component. Once the event invokes, stored the event in a public class variable on the App
Component class, And then made the previously mentioned state value to true
The code looked someting like this:
installPrompt = null;
componentDidMount(){
console.log("Listening for Install prompt");
window.addEventListener('beforeinstallprompt',e=>{
// For older browsers
e.preventDefault();
console.log("Install Prompt fired");
this.installPrompt = e;
// See if the app is already installed, in that case, do nothing
if((window.matchMedia && window.matchMedia('(display-mode: standalone)').matches) || window.navigator.standalone === true){
return false;
}
// Set the state variable to make button visible
this.setState({
installButton:true
})
})
}
The above code is a a part of the App
Class, and then added a function on the class for the button to use:
installApp=async ()=>{
if(!this.installPrompt) return false;
this.installPrompt.prompt();
let outcome = await this.installPrompt.userChoice;
if(outcome.outcome=='accepted'){
console.log("App Installed")
}
else{
console.log("App not installed");
}
// Remove the event reference
this.installPrompt=null;
// Hide the button
this.setState({
installButton:false
})
}
Now in the render function, inside JSX, I used my conditional component to conditionally render the button, also added the function as the onClick
prop of the button:
<Conditional condition={this.state.installButton}
style={styles.installBtn}
onClick={this.installApp}>
Install As Application
</Conditional>
Added some styles to make it look like a sticky button, ...and done!
A note about Static assets:
Static assets like images can be copied in the public folder and used from there in your application, but I noticed that the service worker does not cache them for offline use. So Instead I loaded them directly inside the code, and let webpack handle the rest (keep images in the src
folder for this to work):
import myLogo from './img/logo.png';
And then from inside the render function, with JSX,
<img src={myLogo} alt="Application Logo"/>
btw, I also got warning from ESLint for using the word 'image' inside the alt
prop (attribute) of the img
component(element), so don't do it.
And you are done. Build the application, and host the build
folder. Enjoy your shiny new Progressive web application.
Let me know your feedback on this post.