How to Build and Deploy Superheroes React PWA Using Buddy
"Why do we fall? So that we can learn to pick ourselves back up."
Some quotes don't require a mention of the person who said it! We all can picture an image just with this quote. Superheroes add zeal, inspiration and fun to our lives!
Image loading...
The above screenshot shows the interest in the terms "ironman", "batman" and "superman" on Google Trends over the last 5 years. A value of 100 against the data point Jul 24-30, 2016 shows the peak popularity of Batman worldwide.
Superhero characters have piqued a lot of interest among people. In this article, we'll build a dashboard for listing popular superheroes and their superpowers. The dashboard would showcase the intelligence, speed, strength, power, work, first appearance and many other interesting details of the character.
We'll deploy the application on Buddy CI/CD - powerful and easy-to-configure CI/CD tool for developers. This sure looks like an interesting task! Let's start building it right away!
Sneak Peek: Here's how the Superheroes and Villians dashboard looks like:
Image loading...
Here's a GitHub repository of the project which you can refer to and get to speed!
Bootstrapping the React PWA Project
Let's create a react project using create-react-app package:
bashnpx create-react-app superheros-app --template cra-template-pwa
$
npx
(Node Package Execute) is a tool for executing node packages. We don't have to install create-react-app
package and then execute it. npx
checks if the package is present in the local system and If the package is not present locally, it fetches it from the npm registry and then executes it.
cra-template-pwa
is used for bootstrapping a progressive web application. It adds relevant packages and files in the react project. We'll use some of these features to improve the performance and facilitate an offline experience for the application.
Setting up the Project Structure
Let's now go to the directory where the react project superheros-app
is bootstrapped!
Here's the top-level folder structure for the project:
Image loading...
We would put environment-related configurations in the config
folder. We can have different configuration files such as local.js
, staging.js
and prod.js
.
The constants
folder contains various constants being used across the project. It is a good practice to put constants at once place and use them consistently in all the modules.
The services
folder has just one folder inside it called characters
and it has methods for making the API calls.
The utils
folder is used for keeping utility functions that can be used across the project and the views
folder is used for putting the components/views that are displayed to the end-user. This is a generic template for a react project and helps in maintaining a scalable and modularized structure.
Installing Project Dependencies
We would be using Ant design system for this project. Let's install the antd
package using the below command:
bashnpm install antd --save
$
We can import antd styles in index.css
as:
css@import '~antd/dist/antd.css';
We now have all the required dependencies and can start implementing the views.
Building the view for Characters List
For now, we just have two views
- Header
and SuperHeroList
. This is how App.js
file looks like:
javascriptimport React from 'react'; import { Header } from './views/Header'; import { SuperHeroList } from './views/SuperHeroList'; const App = () => { return ( <> <Header /> <SuperHeroList /> </> ); }; export default App;
The Header
is a simple component and only renders the heading for the app. This is a separate component as it can be reused in other views.
Here's the code in Header.js
file inside the Header
folder:
javascriptimport React from 'react'; import { Typography } from 'antd'; const { Title } = Typography; const Header = () => { return ( <Title>SuperHeroes and Villians from all universes</Title> ); }; export default Header;
As it can be seen from the above code, we have used Typography
from the antd
design system.
Let's move forward and explore the table component that can be used to render the list of characters!
The antd Table
component takes these props - datasource
and columns
that are used for populating data in the table. Here's the list of columns that we would be displaying in the table:
javascriptconst tableColumns = [ { title: '', dataIndex: 'image', render: (image) => { return <Image src={image.url} width={40} alt="superhero" /> }, }, { title: 'SuperHero', dataIndex: 'name', }, { title: 'Alignment', dataIndex: 'alignment', render: (alignment) => { return ( <Tag color={alignment === 'good' ? 'green' : 'volcano'}> {alignment === 'good' ? 'HERO' : 'VILLIAN'} </Tag> ) }, }, { title: 'Full Name', dataIndex: 'fullName', }, { title: 'Height', dataIndex: 'height', }, { title: 'Weight', dataIndex: 'weight', }, { title: 'Race', dataIndex: 'race', }, { title: 'Intelligence', dataIndex: 'intelligence', }, { title: 'Power', dataIndex: 'power', }, { title: 'Speed', dataIndex: 'speed', }, { title: 'Strength', dataIndex: 'strength', }, { title: 'Work', dataIndex: 'work', ellipsis: true, }, ];
Let's put these columns in a file called tableColumns.js
inside the SuperHeroList
folder.
The title
field is used as a display label for the column and dataIndex
is the key that would be picked from the API response payload. We'll use this field once we integrate the cryptocurrency APIs.
The render
field in the column object is used to customize the value that comes in the dataIndex
field. For example, we're rendering an image component based on the image URL that we get in the dataIndex (image)
field. The ellipsis
field is used for larger columns to prevent overflowing of the text to the next row.
Now that we have all the columns for the table, let's prepare the data source for this table component.
We would be using SuperHero API for fetching the characters list and the corresponding details for each of them.
In the next section, we'll implement a utility for making requests to APIs. This a common requests utility function and can be reused across the project.
Setting up requests utility
Let's create a file called requests.js
in the utils
folder and put this code in that file:
javascriptimport isEmpty from './isEmpty'; const request = async (url, data, opts = {}) => { const options = { method: opts.method || 'GET', credentials: opts.credentials || 'same-origin', headers: { 'Content-Type': 'application/json', ...opts.headers, }, }; if (!isEmpty(data)) { if (options.method === 'GET') { url += `?${new URLSearchParams(data).toString()}` } else { options.body = JSON.stringify(data); } } const response = await fetch(url, options); const json = await response.json(); return response.ok ? json : Promise.reject(json); }; export default request;
The above code is easy to grasp but let's put that in simple terms for better understanding!
The request
function takes in url
, data
and some additional options for making the request. The data
parameter contains the request payload and we add it as URL search params in case of GET
requests while it is stringified in other request types such as POST
, PUT
, DELETE
. The opts
parameter is used for taking in request options such as headers
, method
, etc.
We're using ES6 fetch
to make fetch requests and return the response once the promise is resolved. Please note that this fetch
works in the client-side environment. If you have to make fetch
work on the server side, you can use isomorphic fetch.
The isEmpty
util function would be used across the project and here's the implementation of this function:
javascriptconst isEmpty = (input) => { return ( input === undefined || input === null || (typeof input === 'string' && input.trim().length === 0) || (typeof input === 'object' && Object.keys(input).length === 0) ) }; export default isEmpty;
The above code returns true if the input is undefined
, null
, an empty string or if the input is an object and has no keys.
We can use the request method and make API calls to the superhero API endpoints.
Fetching data from the SuperHero API endpoints
Note: You'll have to use an access token for using the superhero APIs. You can get a free access token from here
Let's put this API access token in env.local
as:
defaultREACT_APP_API_ACCESS_TOKEN=<YOUR_API_ACCESS_TOKEN>
Let's now create a file local.js
in the config
folder and put the base API URL in that file! Here's local.js
in config
:
javascriptconst config = { BASE_API_URL: `https://www.superheroapi.com/api.php/${process.env.REACT_APP_API_ACCESS_TOKEN}/`, }; export default config;
The next thing is to write a function in services to get a list of some of the popular characters. Let's do that right away!
We'll create a characters
folder inside the services
folder and here's the code for charactersService.js
:
javascriptimport config from '../../config/local'; import request from '../../utils/request'; import { SUPERHERO_CHARACTER_IDS } from '../../constants/characters'; const getCharacters = async (params) => { const apiPromises = SUPERHERO_CHARACTER_IDS.map((characterId) => request(`${config.BASE_API_URL}${characterId}`)) const response = await Promise.allSettled(apiPromises); return response; }; const charactersService = { getCharacters, }; export default charactersService;
We're making a GET call to https://www.superheroapi.com/api.php/${process.env.REACT_APP_API_ACCESS_TOKEN}/${characterId}
API endpoint.
Promise.allSettled
returns when all the requests in the array are either fulfilled or rejected. In the above code, we're making parallel requests for the characters and using Promise.allSettled
to return back the response to the view.
The constant SUPERHERO_CHARACTER_IDS
is an array of character IDs that we want to display on the user interface. Here's how characters.js
in the constants
folder look like:
javascriptexport const SUPERHERO_CHARACTER_IDS = [69, 644, 370, 720, 346, 149, 332, 204, 156, 165, 687, 162, 659, 226, 498];
We're done with the service function and are ready to call this API from the view component.
Let's modify the SuperHeroList
component as:
javascriptimport React, { useState, useEffect } from 'react'; import { Table, Typography } from 'antd'; import tableColumns, { getExpandedColumns } from './tableColumns'; import charactersService from '../../services/characters/charactersService'; import isEmpty from '../../utils/isEmpty'; const SuperHeroList = () => { const [characters, setCharacters] = useState([]); useEffect(() => { const fetchData = async () => { const response = await charactersService.getCharacters(); const charactersList = response.reduce((acc, promise) => { if (promise.status === 'fulfilled') { const character = promise.value; return [ ...acc, { ...character, key: character.id, fullName: character.biography['full-name'], alignment: character.biography.alignment, height: character.appearance.height[0], heightInCm: character.appearance.height[1], weight: character.appearance.weight[0], race: character.appearance.race, intelligence: character.powerstats.intelligence, power: character.powerstats.power, speed: character.powerstats.speed, strength: character.powerstats.strength, work: character.work.occupation, } ]; } return acc; }, []); setCharacters(charactersList); }; fetchData(); }, []); const renderExpandedDescription = (character) => { const { Paragraph, Title } = Typography; const expandedColumns = getExpandedColumns(character); return ( <Typography> {expandedColumns.map((col) => ( <Typography key={col.key}> <Title level={5}>{col.title}</Title> <Paragraph>{col.value}</Paragraph> </Typography> ))} </Typography> ) } return ( <Table columns={tableColumns} dataSource={characters} expandable={{ expandedRowRender: renderExpandedDescription, rowExpandable: character => character.name !== 'Not Expandable', }} loading={isEmpty(characters)} /> ); }; export default SuperHeroList;
The function fetchData
is used for making the API call and getting the list of characters. Notice the empty dependency array []
which means the useEffect
hook will be executed only once when the component is mounted.
The loading
prop in the Table
component is used to show a loading spinner and as seen in the above code, we're showing a spinner if the characters
array is empty.
The expandable
prop is used for handling the expanded rendering for the rows. The function renderExpandedDescription
is used for rendering the expanded view for a particular row. You can refer to the code for the getExpandedColumns
function here
As seen from the code above, we format the response of the API and return it as:
javascript{ ...character, key: character.id, fullName: character.biography['full-name'], alignment: character.biography.alignment, height: character.appearance.height[0], heightInCm: character.appearance.height[1], weight: character.appearance.weight[0], race: character.appearance.race, intelligence: character.powerstats.intelligence, power: character.powerstats.power, speed: character.powerstats.speed, strength: character.powerstats.strength, work: character.work.occupation, }
We can now relate to the dataIndex
field used in the tableColumns
array. The table component maps the fields using dataIndex
from the data source.
Let's now check the output of all that we have done so far!
Image loading...
You should see a table component with a list of rows displaying various characters.
There is just one thing left in building this application and that's column sorter. It is easy to add sorting in the table component. Let's add that and verify the output!
Here's the sorter
function in tableColumns.js
file:
javascriptconst toNumber = (value) => { return parseFloat(value.split(" ")?.[0]); }; const sorter = (a, b, attr, attrType) => { switch (attrType) { case 'withUnit': return toNumber(a.heightInCm) - toNumber(b.heightInCm); case 'string': if (a[attr] < b[attr]) return -1; if ( a[attr] > b[attr]) return 1; return 0; default: return a[attr] - b[attr]; }; };
The attrType
withUnit
deals with the sorting for height
and weight
columns where the value is along with the respective unit. The string
type attribute is being handled for sorting on the name
column. The number type columns such as power
, intelligence
, speed
fall under the default
case and are sorted simply like numbers.
The sorter
function can be passed to the table column as:
javascriptsorter: (a, b) => sorter(a, b, 'name', 'string'),
Here's the complete tableColumn.js
file!
Image loading...
Setting up Service Worker for Rich Offline Experience
We're making rides to the server to fetch character details on every visit to the website. We can cache these details and directly serve the character list from the cache instead of hitting the server.
A Service Worker is a piece of JavaScript code that intercepts the network requests and can modify the API response. We'll intercept the character requests to the server using a service worker and will first check if the response is present in the cache. If the response is already present in the cache, we'll serve it from there otherwise we'll make a call to the server and store the response in the cache for subsequent visits.
The character details are unlikely to change frequently and hence is the best use case for caching it for a rich offline experience.
Let's do some changes and convert this to an offline-rich PWA!
Please make a change in index.js
file and replace serviceWorkerRegistration.unregister();
to serviceWorkerRegistration.register();
We'll now add a fetch
event listener in service-worker.js
file:
javascriptself.addEventListener('fetch', function(event) { if (!(event.request.url.indexOf('http') === 0)) return; event.respondWith( caches.open('superheroes').then((cache) => { return cache.match(event.request).then((response) => { if (response) { return response; } return fetch(event.request).then((response) => { cache.put(event.request, response.clone()); return response; }); }); }) ); });
We first check if the request is already present in the superheroes
cache and serve it from there.
fetch(event.request)
is used to make a network call to the server to get the character response. We then put this response in the cache for subsequent requests.
Please note: Service workers don't get registered by default on the local environment in applications bootstrapped using create-react-app.
We can verify if the service worker is registered and is working properly by deploying the app on Netlify. Hang on till you reach Cache first & Offline-rich Experience section. We'll deploy the superheroes app on Netlify and would also verify if the service worker is doing the job right!
Now that the application is built, let's deploy it on Buddy!
Quick tutorial on Continuous Integration and Continuous Delivery/Deployment
Let's say that there are 5 developers working on a project and each of these developers is working on some features/bug fixes. Developer 1 is working on the login page, developer 2 is working on the listing page while developer 3 is working on the payments page and the other two developers are fixing bugs for the existing pages.
The developers pick up their tasks at the start of the sprint and begin coding. Everyone has been working hard and has their tasks ready in their feature branches at the end of the sprint. The sprint demo went well and the team now has to deploy these features together in the live environment.
Everyone has a version of truth in their own feature branch!
The team has to converge and make a single source of truth of the codebase that can be deployed with confidence on the production servers.
Continuous Integration is using source code management tools such as GIT, integrating different feature branches and testing the build. It makes sure that the code is stable and in a deployable state.
Image loading...
Continuous Integration allows developers to seamlessly work on the application without worrying about conflicts and ensures that the new changes are integrated into the system without any issues.
The unit tests are successful on the build deployed on the staging/test server. We can now run integration tests and make the production build ready!
Continuous delivery (CD) is a software engineering approach in which teams produce software in short cycles, ensuring that the software can be reliably released at any time and, when releasing the software, without doing so manually. - Wikipedia
Continuous integration and delivery can be achieved using a pipeline of automated steps. Each step can be performed sequentially on the codebase to get stable and ready to release the production build.
Image loading...
In the case of Continous Deployment, the build is automatically deployed to the production server using the pipeline while in the case of Continuous Delivery, we just keep the production build ready and is then manually deployed to the production server.
Here's an illustration to clearly understand the difference between Continous Integration, Continous Delivery and Continous Deployment:
Image loading...
Now that we have built a fundamental understanding of CI/CD. Let's deploy the crypto app using the Buddy CI/CD pipeline. It is easy and straightforward to deploy on Buddy CI/CD and just takes a few minutes to get it going!
Setting up a Deployment Pipeline on Buddy
You can log in to Buddy from here.
You can use the trial version for this project and play around with the pipeline to see if it helps in automating the production build pipeline for your projects.
Buddy has an intuitive user interface and it takes just a moment to get a working pipeline on Buddy CI/CD.
Let's create a Build Deploy & Test pipeline for the superheroes application!
Let's create a new Project:
Image loading...
Select your GitHub repository from the list:
Image loading...
Once you select the source code repository, you'll get an option to create a pipeline:
Image loading...
The pipeline would be executed on every push to the master branch in this case:
Image loading...
You can select the trigger mode as per your preference!
You can put other options such as target website URL, trigger condition, etc but I'm skipping them for now!
Image loading...
We have to set REACT_APP_API_ACCESS_TOKEN
as an environment variable for the project. If you remember, we have not committed env.prod
file in the codebase. We can set up environment variables for the project as:
Image loading...
Click on Add Action from the pipeline header and select Node.js action.
Let's now write the commands that should be executed on the source code:
Image loading...
It is a simple script that installs the npm packages on the staging server and will then create a build.
You can verify if the environment variables are set correctly from here:
Image loading...
This is it! We have set up a working pipeline for the project.
Please note that we have set up an automatic execution on every push for this pipeline but we can also trigger it manually by clicking the Run button on the pipeline
Let's start the first execution (manually) on this pipeline:
Image loading...
We can see from the logs that the npm run build
command is in progress and is creating an optimized production build:
Image loading...
Here's Execution #1:
Image loading...
You can see the detailed logs for this execution from the logs section.
Continuous Integration & Continuous Deployment from Buddy CI/CD Pipeline to the Netlify site
We will deploy the application on Netlify and make a small change in the codebase to check if the pipeline is automatically triggered on push. Let's do this right away!
Let's first add a Netlify action:
Image loading...
You can add one more action by clicking the highlighted +
icon.
Let's add Netlify here:
Image loading...
You'll be asked to authorize Buddy on adding Netlify! Once the authorization is done, you can deploy the crypto GitHub repository on Netlify.
The Netlify site will show up on the Buddy pipeline once the deployment is completed.
Image loading...
You're all set to see Continuous Integration & Continuous Deployment in action!
Let's make a small change in the codebase and push that change to the repository. You'll notice that the pipeline is triggered automatically:
Image loading...
Execution#2 is in progress
Image loading...
This code will also be deployed on the Netlify site:
Image loading...
Image loading...
Sweet! The app is automatically deployed to the target site.
Cache first & Offline-rich Experience
You can check in the Applications tab in Devtools if the service worker is installed and is actiavted.
Let's verify if there is an improvement in the time taken by the network requests for serving the response:
Image loading...
The app looks super fast and loads almost instantly! You can check it for yourself on the Superheroes App on Netlify
Let's now select the offline
option from the network tab and check if the application is still able to provide anything useful to the user:
Image loading...
Awesome! It loads even in the offline mode and that is one of the powerful features of a progressive web application.
Conclusion!
In this article, we built a superheroes PWA using React and antd design system. We learned about Continuous Integration, Continuous Delivery and Continuous Deployment. We also deployed the superheroes application on Buddy CI/CD by creating a pipeline to automatically build test and deploy the application on every push to the master branch.