Progressive Web Apps are experiences that combine the best of the web and the best of apps. They use service workers, HTTPS, a manifest file and an app shell architecture to deliver native app experiences to web applications.
In this tutorial, we’ll build a PWA called PusherCoins. PusherCoins shows the current and past price information about BTC, LTC, and ETH using data from Cryptocurrency. A demo can be seen below. The current Bitcoin, Ether, and Litecoin price will be updated every 10 seconds and the change will be realtime and seen across other connected clients connected via Pusher.
We’re going to be building a realtime PWA with the help of create-react-app.
Some of the common comments made by developers who are just getting into React are that it is hard to set up and there are so many ways to do things.
create-react-app
eliminates all of that by allowing developers to build React apps with little or no build configuration. All you have to do to get a working React app is install the npm module and run a single command.
Most importantly, the production build of create-react-app
is a fully functional Progressive Web Application. This is done with the help of the [sw-precache-webpack-plugin](https://github.com/goldhand/sw-precache-webpack-plugin)
which is integrated into the production configuration.
Let’s get started with building the React app. Install the create-react-app
tool with this command:
npm install -g create-react-app
Once the installation process has been completed, you can now create a new React app by using the command create-react-app pushercoins
.
This generates a new folder with all the files required to run the React app and a service worker file. A manifest file is also created inside the public
folder.
The manifest.json
file in the public
folder is a simple JSON file that gives you, the ability to control how your app appears to the user and define its appearance at launch.
1{ 2 "short_name": "PusherCoins", 3 "name": "PusherCoins", 4 "icons": [ 5 { 6 "src": "favicon.ico", 7 "sizes": "192x192", 8 "type": "image/png" 9 }, 10 { 11 "src": "android-chrome-512x512.png", 12 "sizes": "512x512", 13 "type": "image/png" 14 } 15 ], 16 "start_url": "./index.html", 17 "display": "standalone", 18 "theme_color": "#000000", 19 "background_color": "#ffffff" 20 }
We notify the app of the manifest.json
file by linking to it in line 12 of the index.html
file.
<link rel="manifest" href="%PUBLIC_URL%/manifest.json">
Next up, let’s go through the registerServiceWorker.js
file and see how the service worker file works. The service worker file can be seen in the src
folder on on GitHub.
The service worker code basically registers a service worker for the React app. We first check if the app is being served from localhost via the isLocalhost
const value that will either return a truthy or falsy value. The register()
function helps to register the service worker to the React app only if its in a production mode and if the browser supports Service workers. The unregister()
function helps to unregister the service worker.
Let’s find out if the service worker really works. To do that we’ll need to prepare the React app for production as the Service Worker code only works in production mode. The npm run build
command helps with that.
This command builds the app for production to the build
folder and correctly bundles React in production mode and optimizes the build for the best performance. It also registers the service worker. Run the command and the output from the terminal should look like something below.
We get to see the size of the files in our React app and most importantly how to run the app with the aid of a static server. We are going to use serve npm package to, wait for it, serve(😀) the React app.
Therefore, use the following commands to install serve on your computer and also setup a static server for the app.
1npm i serve -g 2 3 serve -s build
Your application should be up and running at localhost:5000. So how do we check if a site is a PWA? We can do that by checking the service worker section in the Application tab in the Developer tools.
We could also check by using the Lighthouse tool. Lighthouse is an open-source, automated tool for improving the quality of web pages. It has audits for performance, accessibility and progressive web apps. Lighthouse is currently available as an extension on Google Chrome only and as an npm package.
I used the Lighthouse extension to generate a report for the newly created React app in production and got the following result.
The React app got a score of 91 out of 100 for the PWA section, which isn’t that bad. All audits were passed bar the one about HTTPS, which cannot be implemented right now because the app is still on a local environment.
Now that we know how to check if an app is a PWA, let’s go ahead to build the actual app.
As we’ll be building this PWA with React, it’s very important that we think in terms of React components.
Therefore, the React app would be divided into three components.
History.js
houses all the code needed to show the past prices of BTC, ETH, and LTC.Today.js
houses all the code needed to show the current price of BTC, ETH and LTC.App.js
houses both History.js
and Today.js
Alright, let’s continue with building the app. We’ll need to create two folders inside the src
folder, Today
and History
. In the newly created folders, create the files Today.js
, Today.css
and History.js
, History.css
respectively. Your project directory should look like the one below.
Before we get started on the Today
and History
components, let’s build out the app shell.
An app shell is the minimal HTML, CSS and JavaScript required to power the user interface and when cached offline can ensure instant**,** reliably good performance to users on repeat visits. You can read more about app shells here.
Open up the App.js
file and replace with the following code:
1// Import React and Component 2 import React, { Component } from 'react'; 3 // Import CSS from App.css 4 import './App.css'; 5 // Import the Today component to be used below 6 import Today from './Today/Today' 7 // Import the History component to be used below 8 import History from './History/History' 9 10 class App extends Component { 11 render() { 12 return ( 13 <div className=""> 14 <div className="topheader"> 15 <header className="container"> 16 <nav className="navbar"> 17 <div className="navbar-brand"> 18 <span className="navbar-item">PusherCoins</span> 19 </div> 20 <div className="navbar-end"> 21 <a className="navbar-item" href="https://pusher.com" target="_blank" rel="noopener noreferrer">Pusher.com</a> 22 </div> 23 </nav> 24 </header> 25 </div> 26 <section className="results--section"> 27 <div className="container"> 28 <h1>PusherCoins is a realtime price information about<br></br> BTC, ETH and LTC.</h1> 29 </div> 30 <div className="results--section__inner"> 31 <Today /> 32 <History /> 33 </div> 34 </section> 35 </div> 36 ); 37 } 38 } 39 40 export default App;
The App.css
file should be replaced with the following:
1.topheader { 2 background-color: #174c80; 3 } 4 .navbar { 5 background-color: #174c80; 6 } 7 .navbar-item { 8 color: #fff; 9 } 10 .results--section { 11 padding: 20px 0px; 12 margin-top: 40px; 13 } 14 h1 { 15 text-align: center; 16 font-size: 30px; 17 }
We’ll also be using the Bulma CSS framework, so add the line of code below to your index.html
in public
folder.
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bulma/0.4.3/css/bulma.min.css">
Next up, open up the Today.js
file as we’ll soon be writing the code for that component. So what does this component do?
It’s responsible for getting the current prices of Bitcoin, Ether and Litecoin from the Cryptocurrency API and displaying it on the frontend. Let’s write the code.
The first thing we do is import React and its Component module using ES6 import
, we also import axios. axios is used to make API requests to the Cryptocurrency API and can be installed by running npm install axios
in your terminal
1import React, { Component } from 'react'; 2 import './Today.css' 3 import axios from 'axios'
The next thing to do is create an ES6 class named Today
that extends the component module from react
.
1class Today extends Component { 2 // Adds a class constructor that assigns the initial state values: 3 constructor () { 4 super(); 5 this.state = { 6 btcprice: '', 7 ltcprice: '', 8 ethprice: '' 9 }; 10 } 11 // This is called when an instance of a component is being created and inserted into the DOM. 12 componentWillMount () { 13 axios.get('https://min-api.cryptocompare.com/data/pricemulti?fsyms=BTC,ETH,LTC&tsyms=USD') 14 .then(response => { 15 // We set the latest prices in the state to the prices gotten from Cryptocurrency. 16 this.setState({ btcprice: response.data.BTC.USD }); 17 this.setState({ ethprice: response.data.ETH.USD }); 18 this.setState({ ltcprice: response.data.LTC.USD }); 19 }) 20 // Catch any error here 21 .catch(error => { 22 console.log(error) 23 }) 24 } 25 // The render method contains the JSX code which will be compiled to HTML. 26 render() { 27 return ( 28 <div className="today--section container"> 29 <h2>Current Price</h2> 30 <div className="columns today--section__box"> 31 <div className="column btc--section"> 32 <h5>${this.state.btcprice}</h5> 33 <p>1 BTC</p> 34 </div> 35 <div className="column eth--section"> 36 <h5>${this.state.ethprice}</h5> 37 <p>1 ETH</p> 38 </div> 39 <div className="column ltc--section"> 40 <h5>${this.state.ltcprice}</h5> 41 <p>1 LTC</p> 42 </div> 43 </div> 44 </div> 45 ) 46 } 47 } 48 49 export default Today;
In the code block above, we imported the react
and component
class from react. We also imported axios
which will be used for API requests. In the componentWillMount
function, we send an API request to get the current cryptocurrency rate from Cryptocurrency. The response from the API is what will be used to set the value of the state.
Let’s not forget the CSS for the component. Open up Today.css
and type in the following CSS code.
1.today--section { 2 margin-bottom: 40px; 3 padding: 0 50px; 4 } 5 .today--section h2 { 6 font-size: 20px; 7 } 8 .today--section__box { 9 background-color: white; 10 padding: 20px; 11 margin: 20px 0; 12 border-radius: 4px; 13 box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05); 14 } 15 .btc--section { 16 text-align: center; 17 border-right: 1px solid #DAE1E9; 18 } 19 .btc--section h5 { 20 font-size: 30px; 21 } 22 .eth--section { 23 text-align: center; 24 border-right: 1px solid #DAE1E9; 25 } 26 .eth--section h5 { 27 font-size: 30px; 28 } 29 .ltc--section { 30 text-align: center; 31 } 32 .ltc--section h5 { 33 font-size: 30px; 34 } 35 @media (max-width: 480px) { 36 .eth--section { 37 border-right: none; 38 } 39 .btc--section { 40 border-right: none; 41 } 42 .today--section { 43 margin-top: 50px; 44 } 45 }
The next step, is to write the code for History.js
. This component is responsible for showing us the prices of BTC, ETH and LTC from the past five days. We’ll be using the axios
package as well as the moment
package for formatting dates. Moment.js can be installed by running npm install moment
in your terminal. Open up the History.js
file, the first thing we do is import React and its Component module using ES6 import
, we also import axios and Moment.js.
1import React, { Component } from 'react'; 2 import './History.css' 3 import axios from 'axios' 4 import moment from 'moment'
Like we did in the Today.js
component, we’ll create an ES6 class named History
that extends the component module from react
and also create some functions which will be bound with this
.
1class History extends Component { 2 constructor () { 3 super(); 4 this.state = { 5 todayprice: {}, 6 yesterdayprice: {}, 7 twodaysprice: {}, 8 threedaysprice: {}, 9 fourdaysprice: {} 10 } 11 this.getBTCPrices = this.getBTCPrices.bind(this); 12 this.getETHPrices = this.getETHPrices.bind(this); 13 this.getLTCPrices = this.getLTCPrices.bind(this); 14 } 15 // This function gets the ETH price for a specific timestamp/date. The date is passed in as an argument 16 getETHPrices (date) { 17 return axios.get('https://min-api.cryptocompare.com/data/pricehistorical?fsym=ETH&tsyms=USD&ts=' + date); 18 } 19 // This function gets the BTC price for a specific timestamp/date. The date is passed in as an argument 20 getBTCPrices (date) { 21 return axios.get('https://min-api.cryptocompare.com/data/pricehistorical?fsym=BTC&tsyms=USD&ts=' + date); 22 } 23 // This function gets the LTC price for a specific timestamp/date. The date is passed in as an argument 24 getLTCPrices (date) { 25 return axios.get('https://min-api.cryptocompare.com/data/pricehistorical?fsym=LTC&tsyms=USD&ts=' + date); 26 } 27 }
As seen in the code block above, we have defined state values that will hold the price information about BTC, ETH, and LTC for the past five days. We also created functions that returns API requests to Cryptocurrency. Now, let’s write the code that utilizes the functions above and stores the various prices in the state and renders them.
It’s important to note that Cryptocurrency currently does not have an API endpoint that allows you to get a date range of price information. You’d have to get the timestamp of the past five days and then use them individually to get the required data you want.
A workaround will be to use moment.js to get the timestamp of the particular day you want using the .subtract
method and .unix
method . So for example, to get a timestamp of two days ago, you’d do something like:
moment().subtract(2, 'days').unix();
Okay, so let’s continue with the rest of the code and write out the functions that gets the values for the past 5 days.
1// This function gets the prices for the current date. 2 getTodayPrice () { 3 // Get today's date in timestamp 4 let t = moment().unix() 5 // axios.all is used to make concurrent API requests. These requests were the functions we first created and they accept an argument of the date required. 6 axios.all([this.getETHPrices(t), this.getBTCPrices(t), this.getLTCPrices(t)]) 7 .then(axios.spread((eth, btc, ltc) => { 8 let f = { 9 date: moment.unix(t).format("MMMM Do YYYY"), 10 eth: eth.data.ETH.USD, 11 btc: btc.data.BTC.USD, 12 ltc: ltc.data.LTC.USD 13 } 14 // Set the state of todayprice to the content of the object f 15 this.setState({ todayprice: f }); 16 })); 17 } 18 // This function gets the prices for the yesterday. 19 getYesterdayPrice () { 20 // Get yesterday's date in timestamp 21 let t = moment().subtract(1, 'days').unix(); 22 // axios.all is used to make concurrent API requests. These requests were the functions we first created and they accept an argument of the date required. 23 axios.all([this.getETHPrices(t), this.getBTCPrices(t), this.getLTCPrices(t)]) 24 .then(axios.spread((eth, btc, ltc) => { 25 let f = { 26 date: moment.unix(t).format("MMMM Do YYYY"), 27 eth: eth.data.ETH.USD, 28 btc: btc.data.BTC.USD, 29 ltc: ltc.data.LTC.USD 30 } 31 // Set the state of yesterdayprice to the content of the object f 32 this.setState({ yesterdayprice: f }); 33 })); 34 } 35 // This function gets the prices for the two days ago. 36 getTwoDaysPrice () { 37 // Get the date for two days ago in timestamp 38 let t = moment().subtract(2, 'days').unix(); 39 // axios.all is used to make concurrent API requests. These requests were the functions we first created and they accept an argument of the date required. 40 axios.all([this.getETHPrices(t), this.getBTCPrices(t), this.getLTCPrices(t)]) 41 .then(axios.spread((eth, btc, ltc) => { 42 let f = { 43 date: moment.unix(t).format("MMMM Do YYYY"), 44 eth: eth.data.ETH.USD, 45 btc: btc.data.BTC.USD, 46 ltc: ltc.data.LTC.USD 47 } 48 // Set the state of twodaysprice to the content of the object f 49 this.setState({ twodaysprice: f }); 50 })); 51 } 52 // This function gets the prices for the three days ago. 53 getThreeDaysPrice () { 54 // Get the date for three days ago in timestamp 55 let t = moment().subtract(3, 'days').unix(); 56 // axios.all is used to make concurrent API requests. These requests were the functions we first created and they accept an argument of the date required. 57 axios.all([this.getETHPrices(t), this.getBTCPrices(t), this.getLTCPrices(t)]) 58 .then(axios.spread((eth, btc, ltc) => { 59 let f = { 60 date: moment.unix(t).format("MMMM Do YYYY"), 61 eth: eth.data.ETH.USD, 62 btc: btc.data.BTC.USD, 63 ltc: ltc.data.LTC.USD 64 } 65 // Set the state of threedaysprice to the content of the object f 66 this.setState({ threedaysprice: f }); 67 })); 68 } 69 // This function gets the prices for the four days ago. 70 getFourDaysPrice () { 71 // Get the date for four days ago in timestamp 72 let t = moment().subtract(4, 'days').unix(); 73 // axios.all is used to make concurrent API requests. These requests were the functions we first created and they accept an argument of the date required. 74 axios.all([this.getETHPrices(t), this.getBTCPrices(t), this.getLTCPrices(t)]) 75 .then(axios.spread((eth, btc, ltc) => { 76 let f = { 77 date: moment.unix(t).format("MMMM Do YYYY"), 78 eth: eth.data.ETH.USD, 79 btc: btc.data.BTC.USD, 80 ltc: ltc.data.LTC.USD 81 } 82 // Set the state of fourdaysprice to the content of the object f 83 this.setState({ fourdaysprice: f }); 84 })); 85 } 86 // This is called when an instance of a component is being created and inserted into the DOM. 87 componentWillMount () { 88 this.getTodayPrice(); 89 this.getYesterdayPrice(); 90 this.getTwoDaysPrice(); 91 this.getThreeDaysPrice(); 92 this.getFourDaysPrice(); 93 }
So we have five functions above, they basically just use moment.js
to get the date required and then pass that date into the functions we first created above, to get the price information from Cryptocurrency. We use axios.all
and axios.spread
which is a way of of dealing with concurrent requests with callbacks. The functions will be run in the componentWillMount
function.
Finally, for History.js
, we’ll write the render function.
1render() { 2 return ( 3 <div className="history--section container"> 4 <h2>History (Past 5 days)</h2> 5 <div className="history--section__box"> 6 <div className="history--section__box__inner"> 7 <h4>{this.state.todayprice.date}</h4> 8 <div className="columns"> 9 <div className="column"> 10 <p>1 BTC = ${this.state.todayprice.btc}</p> 11 </div> 12 <div className="column"> 13 <p>1 ETH = ${this.state.todayprice.eth}</p> 14 </div> 15 <div className="column"> 16 <p>1 LTC = ${this.state.todayprice.ltc}</p> 17 </div> 18 </div> 19 </div> 20 <div className="history--section__box__inner"> 21 <h4>{this.state.yesterdayprice.date}</h4> 22 <div className="columns"> 23 <div className="column"> 24 <p>1 BTC = ${this.state.yesterdayprice.btc}</p> 25 </div> 26 <div className="column"> 27 <p>1 ETH = ${this.state.yesterdayprice.eth}</p> 28 </div> 29 <div className="column"> 30 <p>1 LTC = ${this.state.yesterdayprice.ltc}</p> 31 </div> 32 </div> 33 </div> 34 <div className="history--section__box__inner"> 35 <h4>{this.state.twodaysprice.date}</h4> 36 <div className="columns"> 37 <div className="column"> 38 <p>1 BTC = ${this.state.twodaysprice.btc}</p> 39 </div> 40 <div className="column"> 41 <p>1 ETH = ${this.state.twodaysprice.eth}</p> 42 </div> 43 <div className="column"> 44 <p>1 LTC = ${this.state.twodaysprice.ltc}</p> 45 </div> 46 </div> 47 </div> 48 <div className="history--section__box__inner"> 49 <h4>{this.state.threedaysprice.date}</h4> 50 <div className="columns"> 51 <div className="column"> 52 <p>1 BTC = ${this.state.threedaysprice.btc}</p> 53 </div> 54 <div className="column"> 55 <p>1 ETH = ${this.state.threedaysprice.eth}</p> 56 </div> 57 <div className="column"> 58 <p>1 LTC = ${this.state.threedaysprice.ltc}</p> 59 </div> 60 </div> 61 </div> 62 <div className="history--section__box__inner"> 63 <h4>{this.state.fourdaysprice.date}</h4> 64 <div className="columns"> 65 <div className="column"> 66 <p>1 BTC = ${this.state.fourdaysprice.btc}</p> 67 </div> 68 <div className="column"> 69 <p>1 ETH = ${this.state.fourdaysprice.eth}</p> 70 </div> 71 <div className="column"> 72 <p>1 LTC = ${this.state.fourdaysprice.ltc}</p> 73 </div> 74 </div> 75 </div> 76 77 </div> 78 </div> 79 ) 80 } 81 } 82 83 export default History;
We can now run the npm start
command to see the app at //localhost:3000.
We can quickly check to see how the current state of this app would fare as a PWA. Remember we have a service worker file which currently caches all the resources needed for this application. So you can run the npm run build
command to put the app in production mode, and check its PWA status with Lighthouse.
We got a 91/100 score. Whoop! The only audit that failed to pass is the HTTPS audit which cannot be implemented right now because the app is still on a local server.
Our application is looking good and fast apparently (Interactive at < 3s), let’s add realtime functionalities by adding Pusher.
By using Pusher, we can easily add realtime functionalities to the app. Pusher makes it simple to bind UI interactions to events that are triggered from any client or server. Let’s setup Pusher.
Log into your dashboard (or create a new account if you’re a new user) and create a new app. Copy your app_id
, key
, secret
and cluster
and store them somewhere as we’ll be needing them later.
We’ll also need to create a server that will help with triggering events to Pusher and we’ll create one with Node.js. In the root of your project directory, create a file named server.js
and type in the following code:
1// server.js 2 const express = require('express') 3 const path = require('path') 4 const bodyParser = require('body-parser') 5 const app = express() 6 const Pusher = require('pusher') 7 8 //initialize Pusher with your appId, key, secret and cluster 9 const pusher = new Pusher({ 10 appId: 'APP_ID', 11 key: 'APP_KEY', 12 secret: 'APP_SECRET', 13 cluster: 'YOUR_CLUSTER', 14 encrypted: true 15 }) 16 17 // Body parser middleware 18 app.use(bodyParser.json()) 19 app.use(bodyParser.urlencoded({ extended: false })) 20 21 // CORS middleware 22 app.use((req, res, next) => { 23 // Website you wish to allow to connect 24 res.setHeader('Access-Control-Allow-Origin', '*') 25 // Request methods you wish to allow 26 res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS, PUT, PATCH, DELETE') 27 // Request headers you wish to allow 28 res.setHeader('Access-Control-Allow-Headers', 'X-Requested-With,content-type') 29 // Set to true if you need the website to include cookies in the requests sent 30 // to the API (e.g. in case you use sessions) 31 res.setHeader('Access-Control-Allow-Credentials', true) 32 // Pass to next layer of middleware 33 next() 34 }) 35 36 // Set port to be used by Node.js 37 app.set('port', (5000)) 38 39 app.get('/', (req, res) => { 40 res.send('Welcome') 41 }) 42 43 // API route in which the price information will be sent to from the clientside 44 app.post('/prices/new', (req, res) => { 45 // Trigger the 'prices' event to the 'coin-prices' channel 46 pusher.trigger( 'coin-prices', 'prices', { 47 prices: req.body.prices 48 }); 49 res.sendStatus(200); 50 }) 51 52 app.listen(app.get('port'), () => { 53 console.log('Node app is running on port', app.get('port')) 54 })
This is a simple Node.js server that uses Express as its web framework. Pusher is initialized with the dashboard credentials, and the various API routes are also defined. Don’t forget to install the packages in use.
npm install express body-parser pusher
We’ll also need to add a line of code to the package.json
file so as to allow API proxying. Since we will be running a backend server, we need to find a way to run the React app and backend server together. API proxying helps with that.
To tell the development server to proxy any unknown requests (/prices/new
) to your API server in development, add a proxy
field to your package.json
immediately after the scripts
object.
"proxy": "http://localhost:5000"
We only need to make the current price realtime and that means we’ll be working on the Today
component, so open up the file. The Pusher Javascript library is needed, so run npm install pusher-js
to install that.
The first thing to do is import the pusher-js
package.
import Pusher from 'pusher-js'
In the componentWillMount
method, we establish a connection to Pusher using the credentials obtained from the dashboard earlier.
1// establish a connection to Pusher 2 this.pusher = new Pusher('APP_KEY', { 3 cluster: 'YOUR_CLUSTER', 4 encrypted: true 5 }); 6 // Subscribe to the 'coin-prices' channel 7 this.prices = this.pusher.subscribe('coin-prices');
We need a way to query the API every 10 seconds so as to get the latest price information. We can use the setInterval
function to send an API request every 10 seconds and then send the result of that API request to Pusher so that it can be broadcasted to other clients.
Before we create the setInterval function, let’s create a simple function that takes in an argument and sends it to the backend server API.
1sendPricePusher (data) { 2 axios.post('/prices/new', { 3 prices: data 4 }) 5 .then(response => { 6 console.log(response) 7 }) 8 .catch(error => { 9 console.log(error) 10 }) 11 }
Let’s create the setInterval
function. We will need to create a componentDidMount
method so we can put the interval code in it.
1componentDidMount () { 2 setInterval(() => { 3 axios.get('https://min-api.cryptocompare.com/data/pricemulti?fsyms=BTC,ETH,LTC&tsyms=USD') 4 .then(response => { 5 this.sendPricePusher (response.data) 6 }) 7 .catch(error => { 8 console.log(error) 9 }) 10 }, 10000) 11 }
So right now, the app queries the API every 10 seconds and sends the data to Pusher, but we still haven’t made the app realtime. We need to implement the realtime functionality so that other clients/users connected to the application can see price change in realtime. That will be done by using Pusher’s bind method.
Inside the componentDidMount
method, add the code below, immediately after the setInterval
function.
1// We bind to the 'prices' event and use the data in it (price information) to update the state values, thus, realtime changes 2 this.prices.bind('prices', price => { 3 this.setState({ btcprice: price.prices.BTC.USD }); 4 this.setState({ ethprice: price.prices.ETH.USD }); 5 this.setState({ ltcprice: price.prices.LTC.USD }); 6 }, this);
The code block above, listens for data from Pusher, since we already subscribed to that channel and uses the data it gets to update the state values, thus, realtime changes.
We now have Progressive Realtime App! See a demo below.
Right now, if we were to go offline, our application would not be able to make API requests to get the various prices. So how do we make sure that we still able to see some data even when the network fails?
One way to go about it would be to use Client Side Storage. So how would this work? We’ll simply use localStorage to cache data.
localStorage makes it possible to store values in the browser which can survive the browser session. It is one type of the Web Storage API, which is an API for storing key-value pairs of data within the browser. It has a limitation of only storing strings. That means any data being stored has to be stringified with the use of JSON.stringify
It’s important to note that there are other types of client side storage, such as Session Storage, Cookies, IndexedDB, and WebSQL. Local Storage can be used for a demo app like this, but in a production app, it’s advisable to use a solution like IndexedDB which offers more features like better structure, multiple tables and databases, and more storage.
The goal will be to display the prices from localStorage. That means we’ll have to save the results from various API requests into the localStorage and set the state to the values in the localStorage. This will ensure that when the network is unavailable and API requests are failing, we would still be able to see some data, albeit cached data. Let’s do just that. Open up the Today.js
file and edit the code inside the callback function of the API request to get prices with the one below.
1axios.get('https://min-api.cryptocompare.com/data/pricemulti?fsyms=BTC,ETH,LTC&tsyms=USD') 2 .then(response => { 3 this.setState({ btcprice: response.data.BTC.USD }); 4 localStorage.setItem('BTC', response.data.BTC.USD); 5 6 this.setState({ ethprice: response.data.ETH.USD }); 7 localStorage.setItem('ETH', response.data.ETH.USD); 8 9 this.setState({ ltcprice: response.data.LTC.USD }); 10 localStorage.setItem('LTC', response.data.LTC.USD); 11 }) 12 .catch(error => { 13 console.log(error) 14 })
We are essentially storing the values gotten from the API request to the localStorage. With our values now in the localStorage, we’ll need to set the state values to the saved values in localStorage. Inside the componentDidMount
method, before the setInterval
code, add the following code.
1if (!navigator.onLine) { 2 this.setState({ btcprice: localStorage.getItem('BTC') }); 3 this.setState({ ethprice: localStorage.getItem('ETH') }); 4 this.setState({ ltcprice: localStorage.getItem('LTC') }); 5 }
The code above is only executed when the browser is offline. We can check for internet connectivity by using navigator.onLine
. The navigator.onLine
property returns the online status of the browser. The property returns a boolean value, with true
meaning online and false
meaning offline.
Let’s now implement localStorage for History.js
too. We’ll need to save the values gotten from the API in these functions ( getTodayPrice(), getYesterdayPrice(), getTwoDaysPrice(), getThreeDaysPrice(), this.getFourDaysPrice()
) to the localStorage.
1// getTodayPrice() 2 localStorage.setItem('todayprice', JSON.stringify(f)); 3 this.setState({ todayprice: f }); 4 5 // getYesterdayPrice() 6 localStorage.setItem('yesterdayprice', JSON.stringify(f)); 7 this.setState({ yesterdayprice: f }); 8 9 // getTwoDaysPrice() 10 localStorage.setItem('twodaysprice', JSON.stringify(f)); 11 this.setState({ twodaysprice: f }); 12 13 // getThreeDaysPrice() 14 localStorage.setItem('threedaysprice', JSON.stringify(f)); 15 this.setState({ threedaysprice: f }); 16 17 // getFourDaysPrice() 18 localStorage.setItem('fourdaysprice', JSON.stringify(f)); 19 this.setState({ fourdaysprice: f });
We are essentially storing the values gotten from the API request to the localStorage. With our values now in the localStorage, we’ll also need to set the state values to the saved values in localStorage like we did in the Today
component. Create a componentDidMount
method and add the following code inside the method.
1componentDidMount () { 2 if (!navigator.onLine) { 3 this.setState({ todayprice: JSON.parse(localStorage.getItem('todayprice')) }); 4 this.setState({ yesterdayprice: JSON.parse(localStorage.getItem('yesterdayprice')) }); 5 this.setState({ twodaysprice: JSON.parse(localStorage.getItem('twodaysprice')) }); 6 this.setState({ threedaysprice: JSON.parse(localStorage.getItem('threedaysprice')) }); 7 this.setState({ fourdaysprice: JSON.parse(localStorage.getItem('fourdaysprice')) }); 8 } 9 }
Now our application will display cached values when there’s no internet connectivity.
It’s important to note that the app is time sensitive. Time sensitive data are not really useful to users when cached. What we can do is, add a status indicator warning the user when they are offline, that the data being shown might be stale and an internet connection is needed to show the latest data.
Now that we’re done building, let’s deploy the app to production and carry out a final Lighthouse test. We’ll be using now.sh for deployment, now
allows you to take your JavaScript (Node.js) or Docker powered websites, applications and services to the cloud with ease. You can find installation instructions on the site. You can also use any other deployment solution, I’m using Now because of its simplicity.
Prepare the app for production by running the command below in the terminal
npm run build
This builds the app for production to the build
folder. Alright, so the the next thing to do is to create a server in which the app will be served from. Inside the build
folder, create a file named server.js
and type in the following code.
1const express = require('express') 2 const path = require('path') 3 const bodyParser = require('body-parser') 4 const app = express() 5 const Pusher = require('pusher') 6 7 const pusher = new Pusher({ 8 appId: 'APP_ID', 9 key: 'YOUR_KEY', 10 secret: 'YOUR SECRET', 11 cluster: 'YOUR CLUSTER', 12 encrypted: true 13 }) 14 15 app.use(bodyParser.json()) 16 app.use(bodyParser.urlencoded({ extended: false })) 17 app.use(express.static(path.join(__dirname))); 18 19 app.use((req, res, next) => { 20 // Website you wish to allow to connect 21 res.setHeader('Access-Control-Allow-Origin', '*') 22 // Request methods you wish to allow 23 res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS, PUT, PATCH, DELETE') 24 // Request headers you wish to allow 25 res.setHeader('Access-Control-Allow-Headers', 'X-Requested-With,content-type') 26 // Set to true if you need the website to include cookies in the requests sent 27 // to the API (e.g. in case you use sessions) 28 res.setHeader('Access-Control-Allow-Credentials', true) 29 // Pass to next layer of middleware 30 next() 31 }) 32 33 app.set('port', (5000)) 34 35 app.get('/', (req, res) => { 36 res.sendFile(path.join(__dirname + '/index.html')); 37 }); 38 39 app.post('/prices/new', (req, res) => { 40 pusher.trigger( 'coin-prices', 'prices', { 41 prices: req.body.prices 42 }); 43 res.sendStatus(200); 44 }) 45 46 app.listen(app.get('port'), () => { 47 console.log('Node app is running on port', app.get('port')) 48 })
This is basically the same code we wrote in the server.js
file in the root of the project directory. The only addition here is that we set the home route to serve the index.html
file in the public
folder. Next up, run the command npm init
to create a package.json
file for us and lastly install the packages needed with the command below.
npm install express body-parser pusher
You can now see the application by running node server.js
inside the build
folder and your app should be live at localhost:5000
Deploying to Now is very easy, all you have to do is run the command now deploy
and Now takes care of everything, with a live URL automatically generated.
If everything goes well, your app should be deployed and live now, in this case, https://build-zrxionqses.now.sh/. Now automatically provisions all deployments with SSL, so we can finally generate the Lighthouse report again to check the PWA status. A live Lighthouse report of the site can be seen here.
100/100! Whoop! All the PWA audits were passed and we got the site to load under 5s (2.6s).
One of the features of PWAs is the web app install banner. So how does this work? A PWA will install a web app install banner only if the following conditions are met:
short_name
(used on the home screen)name
(used in the banner)image/png
)start_url
that loadsThe manifest.json
file in the public
folder meets all the requirements above, we have a service worker registered on the site and the app is served over HTTPS at https://build-zrxionqses.now.sh/.
In this tutorial, we’ve seen how to use ReactJS, Pusher and Service Workers to build a realtime PWA. We saw how service workers can be used to cache assets and resources so as to reduce the load time and also make sure that the app works even when offline.
We also saw how to use localStorage to save data locally for cases when the browser looses connectivity to the internet.
The app can be viewed live here and you can check out the Github repo here. See if you can change stuff and perhaps make the app load faster!