ISRO React App - Working on the Components - Part 2

Zaki Mohammed Zaki Mohammed
Aug 03, 2023 | 7 min read | 385 Views | Comments

The time has brought us to talk about the components of ISRO React App. In this article we will continue to talk about the remnants of the app. How the services been created, what are the components present to show the listing of ISRO API data.

If you haven't read the previous article regarding the initial setup of the app, then this is very good time to check it out before beginning with this one. The part 1 has the details about initial setup of ISRO React App you can check it out from here ISRO React App - Initial Setup - Part 1. Let us construct components and services for the amazing ISRO-API

We will head in this direction:

  • Creating Axios Services
  • Configuring Routes
  • Designing List Component
  • Designing Search Component

Creating Axios Services

Without further ado, let us create services those will make actual calls to the ISRO-API for the data. We will create methods for each of the GET endpoints.

For this we have created a services folder, inside of it a "core.service.ts" will hold up the Axios configuration code and a generic GET method to get the API response.

// services/core.service.ts

import axios from 'axios';
import { urlIsro } from '../constants/core.constant';

const axiosInstance = axios.create({
  baseURL: urlIsro.base,
});

const get = async <T>(url: string): Promise<T> => {
  const response = await axiosInstance.get(url);
  if (response.status === 200) {
    return response.data as T;
  } else {
    throw new Error(`[${response.status}]: ${response.statusText}`);
  }
};

const service = {
  get,
};

export default service;

Here, the axiosInstance configures the baseURL of the ISRO-API. Later, a generic get method takes URL and return supplied typed response. Consider this file to have your wrapper methods for the API calls in future for POST, PUT, DELETE sort of operations.

On the basis of this we have app.service.ts file that holds up the ISRO-API endpoint calls.

// services/app.service.ts

import { urlIsro, urlRestCountries } from '../constants/core.constant';
import { Country } from '../models/Country';
import { CentreDto, CustomerSatelliteDto, LauncherDto, SpacecraftDto } from '../models/Dto';
import core from './core.service';

const getSpacecrafts = async () => {
  return await core.get(urlIsro.spacecrafts);
};

const getLaunchers = async () => {
  return await core.get(urlIsro.launchers);
};

const getCustomerSatellites = async () => {
  return await core.get(urlIsro.customerSatellites);
};

const getCentres = async () => {
  return await core.get(urlIsro.centres);
};

const getCountries = async () => {
  return await core.get(`${urlRestCountries.base}${urlRestCountries.all}`);
};

const service = {
  getSpacecrafts,
  getLaunchers,
  getCustomerSatellites,
  getCentres,
  getCountries,
};

export default service;

Here, we are using the core service get method to make the calls based on the endpoints. Note that we have also created a constant file to hold up the app level constants values. We are exposing these method to be consumed by the components.

Configuring Routes

Now, create bunch of routed components, club them into a folder name "pages".

pages
|-- About.tsx
|-- Centre.tsx
|-- CustomerSatellite.tsx
|-- Home.tsx
|-- Launcher.tsx
|-- Spacecraft.tsx

Register these components in the route configuration, which is present in App.tsx file:

<Routes>
  <Route path='/' element={<Home />} />
  <Route path='/spacecrafts' element={<Spacecraft />} />
  <Route path='/launchers' element={<Launcher />} />
  <Route path='/customer-satellites' element={<CustomerSatellite />} />
  <Route path='/centers' element={<Centre />} />
  <Route path='/about' element={<About />} />
  <Route path='*' element={<Navigate to={'/'} />} />
</Routes>

Here, we are configuring the routes with corresponding components.

This app is hosted at Netlify so for making React Router to work without 404 on direct path hits, we need to add a file named "_redirects" to public folder. The content of this fill will be like this:

/*  /index.html 200

Designing List Component

The components Spacecraft, Launcher, CustomerSatellite and Centre does more or less the same thing with only UI differences. We will take one of such components (Spacecraft) and understand how its implemented, rest components are following the same pattern.

The Spacecraft components shows the data from the spacecraft endpoint of the ISRO-API. Let us break down the Spacecraft component.

The initial setup of Spacecraft components looks like this:

// pages/Spacecraft.ts

import Container from '../components/Container';
import PageTitle from '../components/PageTitle';

const Spacecraft = () => {
  return (
    <Container>
      <PageTitle title="Spacecraft" icon={'rocket'} />
    </Container>
  );
};

export default Spacecraft;

Here, we have created a Container component for wrapping within TailwindCSS container classes. There is a PageTitle component too that shows the page title and icon related to the page.

Let us add the dispatch and selector hooks:

// pages/Spacecraft.ts

...
import { useAppDispatch, useAppSelector } from '../hooks';
import {
  selectSpacecraftsFiltered,
  selectSearchValue,
} from '../reducers/SpacecraftReducers';

const Spacecraft = () => {
  const filtered = useAppSelector(selectSpacecraftsFiltered);
  const searchValue = useAppSelector(selectSearchValue);
  const dispatch = useAppDispatch();
  
  ...

  return (...);
};

export default Spacecraft;

Here, we have added the hooks for selector and dispatch those we have created in the part 1 (previous article). Using these hooks we are creating the selectors namely filtered and searchValue constants and dispatch a method using which we will invoke the actions.

Let us call the ISRO-API to get the data on the page load, for this we will be needing useEffect() hook:

// pages/Spacecraft.ts

...
import { useEffect } from 'react';
import appService from '../services/app.service';

const Spacecraft = () => {
  ...
  
  useEffect(() => {
    const getAll = async () => {
      try {
        const data = await appService.getSpacecrafts();
        console.log(data);
      } catch (error) {
        console.log('Error Occurred', error);
      }
    };

    getAll();
  }, []);

  return (...);
};

export default Spacecraft;

Here, we are making an network call to ISRO-API spacecraft endpoint. The data will be logged to console.

Let us dispatch the actions now based on the sequence of operations:

// pages/Spacecraft.ts

...
import { useEffect } from 'react';
import appService from '../services/app.service';
import { setLoading } from '../reducers/AppReducers';
import {
  getAll as getSpacecrafts,
} from '../reducers/SpacecraftReducers';

const Spacecraft = () => {
  ...
  
  useEffect(() => {
    const getAll = async () => {
      try {
        dispatch(setLoading(true));
        const data = await appService.getSpacecrafts();
        dispatch(getSpacecrafts(data.spacecrafts));
      } catch (error) {
        console.log('Error Occurred', error);
      } finally {
        dispatch(setLoading(false));
      }
    };

    if (filtered === null) {
      getAll();
    }
  }, []);

  return (...);
};

export default Spacecraft;

Here, we have dispatched the corresponding actions like setLoading and getSpacecrafts as per the need. Also, notice that we are calling getAll() method within useEffect() hook only when the filtered is null, otherwise the data is already present and not need to call the API.

Let us now work on the listing of the spacecraft data in a tabular format:

// pages/Spacecraft.ts

...
import Container from '../components/Container';
import PageTitle from '../components/PageTitle';
import NoRecords from '../components/NoRecords';
import Icon from '../components/Icon';

const Spacecraft = () => {
  ...

  return (
    <Container>
      <PageTitle title="Spacecraft" icon={'rocket'} />
	  
      {filtered && (
        <div className="overflow-x-auto">
          <table className="table table-zebra">
            <thead>
              <tr>
                <th className="w-1">#</th>
                <th>Name</th>
              </tr>
            </thead>
            <tbody>
              {filtered.map(item => (
                <tr key={item.id}>
                  <td>{item.id}</td>
                  <td>
                    <div className="flex items-center space-x-3">
                      <div className="text-4xl">
                        <Icon icon="rocket" classes="w-10" />
                      </div>
                      <div>
                        <div className="font-bold">{item.name}</div>
                        <div className="text-sm opacity-50">Spacecraft</div>
                      </div>
                    </div>
                  </td>
                </tr>
              ))}
            </tbody>
          </table>
        </div>
      )}

      <NoRecords filtered={filtered} />
	  
    </Container>
  );
};

export default Spacecraft;

Here, we created a responsive table using TailwindCSS and DaisyUI. Nothing fancy here, just using our Icon component to create the rocket icon. At the very end we have our NoRecords component that shows no entry if data is not found after doing a search. Notice that we are using the "filtered" selector constant for iterating the rows.

Let us put the last piece of the puzzle, the Search component.

Designing Search Component

We have created a Search component that will do the searching within the given list irrespective of where its been used. It will work on the supplied data. First we will see its application and usage in our list component of Spacecraft.

// pages/Spacecraft.ts

...
import Container from '../components/Container';
import PageTitle from '../components/PageTitle';
import {
  clearSearch,
  search,
  selectSearchValue,
} from '../reducers/SpacecraftReducers';
import Search from '../components/Search';

const Spacecraft = () => {
  ...
  const searchValue = useAppSelector(selectSearchValue);

  return (
    <Container>
      <PageTitle title="Spacecraft" icon={'rocket'} />

      <Search
        placeholder="Search spacecrafts..."
        value={searchValue}
        search={search}
        clearSearch={clearSearch}
      />

      ...
	  
    </Container>
  );
};

export default Spacecraft;

Here, we have added the Search component that takes 4 inputs, first the placeholder text that will be displayed to the user. Second the search value, this value is provided by one of the selectors named selectSearchValue; the reason to provide the selector's value is to keep it updated through store and not from local value, so when user traverse back and forth between pages the search value will still be persisted providing great user experience. We then providing the search and clearSearch actions to the Search component; so it will dispatch these actions when user does some UI operations (like entering search text or clicking on clear button). 

Now, let us look into the inside of Search component:

// components/Search.tsx

import { ChangeEvent } from 'react';
import { useAppDispatch } from '../hooks';
import { SearchModel } from '../models/Search';
import iconCancel from './../assets/icons/cancel.png';

const Search = ({ placeholder, value, search, clearSearch }: SearchModel) => {
  const dispatch = useAppDispatch();

  const onSearch = (event: ChangeEvent<HTMLInputElement>) => {
    const value = event.target.value?.toLowerCase();
    if (!value) {
      dispatch(clearSearch());
      return;
    }

    dispatch(search(value));
  };

  const onClearSearch = () => {
    dispatch(clearSearch());
  };

  return (
    <>
      <div className="flex mb-5">
        <input
          type="text"
          placeholder={placeholder}
          className="input input-bordered w-full"
          onChange={onSearch}
          value={value || ''}
        />
        <button className="btn btn-light ms-3" onClick={onClearSearch}>
          <img src={iconCancel} alt="" className="w-4 inline m-0" />
        </button>
      </div>
    </>
  );
};

export default Search;

Here, we are getting these 4 props and using them appropriately.

placeholder: Used inside the input's placeholder attribute.

value: Assigned to the value attribute of the input control (if the value is not truthy then its kept empty).

search: The search action method is invoked within the onSearch event handler method only if the search text is provided.

clearSearch: The clearSearch action method is invoked when user clicks on the clear button or user clears out the search text from the input control.

Rest of the UI components are simple and self-explanatory like Icon, Container, PageTitle, etc. those are built using TailwindCSS and DaisyUI. Another worth to mention item is the usage of another API for the country flags that is REST Countries. This API is used in the Customer Satellite page where the ISRO deployed the satellites of different countries into the space. To show the flags of these countries we are using the REST Countries API.


Zaki Mohammed
Zaki Mohammed
Learner, developer, coder and an exceptional omelet lover. Knows how to flip arrays or omelet or arrays of omelet.