How to Build and Deploy Superheroes React PWA Using Buddy

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!

Interest on Google Trends

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:

Superheroes App

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:

npx 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:

Folder Structure

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:

npm install antd --save

We can import antd styles in index.css as:

@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:

import 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:

import 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:

const 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:

import 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:

const 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:

REACT_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:

const 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:

import 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:

export 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:

import 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:

{
  ...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!

Superheroes App dashboard

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:

const 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:

sorter: (a, b) => sorter(a, b, 'name', 'string'),

Here's the complete tableColumn.js file!

Superheroes App dashboard

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:

self.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.

Continuous Integration

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.

CI/CD pipeline

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:

Continous Integration, Continous Delivery and Continous Deployment comparison

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:

Creating a new project

Select your GitHub repository from the list:

Selecting GitHub repository

Once you select the source code repository, you'll get an option to create a pipeline:

Adding pipeline

The pipeline would be executed on every push to the master branch in this case:

Pipeline setup

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!

Pipeline - other options

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:

Environment variables

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:

Pipeline - run script

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:

Verification of the environment variables

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:

Execution of the pipeline

We can see from the logs that the npm run build command is in progress and is creating an optimized production build:

Build progress

Here's Execution #1:

Execution - passed

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:

Adding Netlify action

You can add one more action by clicking the highlighted + icon.

Let's add Netlify here:

Adding Netlify action

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.

Netlify action setup

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:

Execution 2

Execution#2 is in progress

Execution 2 - progress

This code will also be deployed on the Netlify site:

Execution 2 - Netlify

Execution 2 - Netlify - logs

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:

Service worker cache

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:

Offline app

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.

About the Author
Ankita Masand

Ankita Masand

Ankita is a Freelance Software Engineer with expertise in React, NodeJs, MongoDB, and GraphQL. She likes solving engineering problems and believes every complicated problem can be solved with ease by breaking it down to fundamental subsets. She loves writing technical blogs and makes it a point to explain convoluted technical jargon in simple and concise language.

The Web Dev Monthly

Sign up for a free monthly scoop of news and features articles handpicked by our staff.

Unsubscribe at any time. No hidden catch.