How to build a React application that integrates with the GitHub API and utilizes various React libraries.

How to build a React application that integrates with the GitHub API and utilizes various React libraries.

What is React?

React is a JavaScript library used for building user interfaces. Some advantages of React include,

  • Reusability of components. - It makes use of virtual DOM which improves performance.

  • It is easier to create dynamic web applications with it due to its declarative approach.

There are other benefits of using React and you can click here to see more.

What is an API?

Application Programming Interface (API) can be that of the web, browser, server or third party.

A Web API helps in extending the functionality of the browser, and in simplifying complex functions.

A Browser API which comprises Web APIs helps in extending the functionality of a web browser. e.g, Geolocation API, Fetch API, etc.

A Server API helps in extending the functionality of a web server. A Third party API is not available in the browser but can be downloaded from the web. An example is the GitHub API which we can use to fetch our GitHub details.

What is a library?

React itself is a JavaScript library and not a framework though developers tend to use the terms “library” and “framework” interchangeably. Check out this to learn more about the differences between a library and a framework as that is not the essence of this post.

A library is a collection of reusable, compiled, and tested code that can facilitate the automation or augmentation of application functionalities.

Simply put, a library comprises codes that have been written by other people which we can then use to hasten our development processes. A library eliminates the need for writing code for complex functions and it prevents us from having to write code to solve the same problem over and over again and as well reduces application development costs.

To integrate some features on a React application, some packages (libraries) are needed. In the project we will be building throughout this article, some of the libraries we will be working with are React-Router, React-helmet-async, React-Icons, and React-Error-Boundary.

Now that we have some fundamental knowledge of the topic, let us start building the application.

Getting Started

Pre-requisites for local development

The prerequisite tool for local development is:

  • Node

How to create a React application?

In this article, we will be creating a React application using the Command Line Interface (CLI), on your CLI, navigate to the directory that will hold the application and run the code below.

npx create-react-app github-porfolio
  • npx is a package runner tool that comes with npm.

  • github-portfolio is the root folder of the application we are going to build.

To start the development server, run the following

cd github-porfolio
npm start or npm run start

By default, the front end will run on localhost:3000, so open the URL in your browser and you should be having what I have in the image below.

Let us continue by cleaning up this default app and also creating and organizing our project directories.

Some folders I will be creating inside of the src folder are,

  • assets: This will hold all images or other assets that will be used in the component.

  • components: This will hold all the reusable components.

  • hooks: This will hold all the custom hooks that will be created.

  • pages: This will hold the file that will serve as a page.

  • store: This will hold our application-wide state. We will be using the useContext API for this feature in this application.

  • stylesheets: This will hold all the cascading style sheets(CSS) for the application.

Final Project File Structure.

├── README.md
├── node_modules
├── public
├── src
├──  ├──  assets
├──  ├──  components
├──  ├──   ├── Header
├──  ├──   ├── Navigation
├──  ├──   ├── Footer
├──  ├──   ├── Pagination
├──  ├──   ├── Repository
├──  ├──   ├── UI
├──  ├──   ├── UserDetails
├──  ├──  hooks
├──  ├──   ├── useFetch.jsx
├──  ├──  pages
├──  ├──   ├── Home
├──  ├──   ├──  ├── AppHome.jsx
├──  ├──   ├──  ├── Home.jsx
├──  ├──   ├── ErrorBoundary
├──  ├──   ├──  ├── ErrorBoundary.jsx
├──  ├──   ├── ErrorPage
├──  ├──   ├──  ├── ErrorPage.jsx
├──  ├──  store
├──  ├──   ├── DataContextProvider.jsx
├──  ├──  Stylesheets (Houses all of our stylings)
├── App.jsx (Root component)
├── index.js (Loaded or Mounted JavaScript file)
├── gitignore
└── package.json

Installing some other React libraries.

Below is what we aim to achieve in this project and these are what we will be implementing throughout this article.

- Fetch your GitHub deatils using the GitHub API.
- Create a page with a list of all your Repositories.
- Create a page that show the details of a single repository using nested routing.
- Implement proper SEO.
- Implement Error Boundary.
- Implement a page that will catch invalid urls.

To implement some of these features, we will work with some libraries and they include,

  • React Router: This is a standard library for routing in React and that enables the navigation among views of various components in a React application.

  • React Icons: This library is composed of popular icons and it allows us to use the ES6 import syntax to only import the icon needed in our application.

  • React Error Boundary: This is used to add an Error boundary component that catches JavaScript errors anywhere in its child component tree and displays a fallback UI in place of the component tree that crashed.

  • React Helmet Async: This is a library that allows one to customize one's document head.

  • Node Sass: This is a library that allows one to write styles using the CSS pre-processor (SCSS).

To install all these packages at once, we can run the following commands

npm install react-router-dom react-icons react-helmet-async react-error-boundary node-sass

After the installation of all the aforementioned packages, your package.json file should look just like the snapshot below.

{
  "name": "github_portfolio",
  "version": "0.1.0",
  "private": true,
  "dependencies": {
    "@testing-library/jest-dom": "^5.16.5",
    "@testing-library/react": "^13.4.0",
    "@testing-library/user-event": "^13.5.0",
    "node-sass": "^7.0.3",
    "react": "^18.2.0",
    "react-dom": "^18.2.0",
    "react-error-boundary": "^3.1.4",
    "react-helmet-async": "^1.3.0",
    "react-icons": "^4.4.0",
    "react-router-dom": "^6.4.0",
    "react-scripts": "5.0.1",
    "sass": "^1.54.9"
  },
  "scripts": {
    "start": "react-scripts start",
    "build": "react-scripts build",
    "test": "react-scripts test",
    "eject": "react-scripts eject"
  },
  "eslintConfig": {
    "extends": [
      "react-app",
      "react-app/jest"
    ]
  },
  "browserslist": {
    "production": [
      ">0.2%",
      "not dead",
      "not op_mini all"
    ],
    "development": [
      "last 1 chrome version",
      "last 1 firefox version",
      "last 1 safari version"
    ]
  }
}

Now, let us get started by integrating routing.

Integrating Routing

Do not forget that we already installed a package for this sole purpose and to get started,

The first step to take is to,

  • Import BrowserRouter as a named import from the package (react-router-dom).
import { BrowserRouter } from "react-router-dom"
  • Wrap our whole application with it as shown below.
<BrowserRouter> <App /> </BrowserRouter>

latest index.js file snapshot

import React from "react";
import ReactDOM from "react-dom/client";

// The component that enables routing.
import { BrowserRouter } from "react-router-dom";

// Our App root component
import App from "./App";

// All stylings
import "./Stylesheets/main.scss";

const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
  <BrowserRouter>
    <App />
  </BrowserRouter>
);

Sequel to this, we can proceed to use Routes and Route components in our application as they can only work inside an application that is wrapped with the BrowserRouter component.

From the project we are trying to build as we saw earlier, we will be having 4 paths (3 Route(s) and a nested Route) and they are,

  • "/home": A page where all details from the GitHub API will be displayed.

  • "/home/:id": A page to show the details of a particular repository using the given id. This will be the nested route

  • "/errorboundary": A page to test the error boundary component.

  • "/404page": A page to test the 404 (Not found) page component.

Whenever you start your application, no path will be appended to your host and so there will be a need for you to navigate to /home as that is the first page we want to see.

To implement this, we will be needing another component from react-router-dom, the Navigate component.

Let us go ahead and implement these routings in our root file (App.jsx) by following the steps below,

  • Import Routes, Route, and Navigate as a named import from react-router-dom.

      Import {Navigate, Route, Routes} from "react-router-dom"
    

Things you should know before proceeding are,

  • You can have multiple Route components in a Routes component

  • The Route should have at least two attributes which are the path and element

  • The path attributes take in either the absolute or relative path in the string.

  • The element attributes take in the component that you want to render on the path accustomed to the Route.

All these you can see in the App.jsx file latest snapshot below

import { Navigate, Route, Routes } from "react-router-dom";

const App = () => {
  return (
    <Routes>
      <Route path="/" element={<Navigate to="/home" />} />
      <Route path="/home" element={<div>Home page</div>} />
      <Route path="/errorboundary" element={<div>Error Boundary page</div>} />
      {/* Routes that will be matched if none of the Route(s) above is matched */}
      <Route path="*" element={<div>Error 404 page</div>} />
    </Routes>
  );
};
export default App;

The Navigate component helps us to automatically redirects our application from host URL to host/home URL.e.g, localhost:3000 to localhost:3000/home. The component takes an attribute to which takes the path to the Route we want to automatically navigate to.

The Route that has * as its path will be matched and rendered whenever none of the Route(s) above it gets matched, so this is the Route that will be displayed whenever the user goes to an invalid URL.

How to implement a Nested route in React

With my little experience working with React, there are two ways in which I can implement nested routing and the one you use just depend on how you want to structure your code though I will be demonstrating one out of the two ways.

If you are the type that will like to have all your routing logic in a single file, then you would prefer the method I will be using in this article but if you want your logic separated, then you can read the other way of implementing it.

So, what is Nested Routing? Simply put, nested routing is having a Route component inside the opening and closing tag of another Route component as shown below

<Routes>
  <Route path="/home" element={<AppHome />}>
    // Parent route
    <Routes path="" element={<div>A new page </div>} /> // A child route. path="/home"
    <Route path=":id" element={<div>A new page</div>} />
    //Another child route. path="/home/anyID"
  </Route>
</Routes>

Some things to take note of are,

  • The path of the children's route must be relative to the parent route.

  • If you want the children route component content to be rendered on an entirely new page that doesn't have the parent route component contents, then you have to create a route for the parent component also but the route path will be an empty string as shown above.

In the snapshot above, we have just two route(s), the one to show the parent content ("/home") and the one to show the single repository content ("/home/:id").

There is still one more thing we have to do before our nested routing logic will work but before we do just that, let us create a subfolder inside our pages folder and name it Home. Inside this subfolder, let us create the component rendered on the parent Route AppHome.jsx file and finish setting up our nested routing logic.

  • Go to the component rendered on the parent route in our case the AppHome component,

  • Import Outlet from react-router-dom and return it from the AppHome component as shown below.

import { Outlet } from "react-router-dom";

const AppHome = () => {
  return (
    <>
      {/* This enables the nested route(s) to show */}
      <Outlet />
    </>
  );
};
export default AppHome;

And with this, we are done implementing the logic.

Next, we will create all our pages folders and files to attain what we saw in the Final Project Structure earlier.

Since we will be having three main Route(s), we need to have two more subfolders in our pages folder. the files we will be creating are,

  • ErrorPage.jsx: This is the component that will be rendered whenever a user goes an invalid URL. e.g "/404page". Below is the codes the component is made up

      // This is a hook from react-router-dom to redirect dynamically
      import { useNavigate } from "react-router-dom";
    
      // Already created components
      import Button from "../../components/UI/Button/Button";
      import Card from "../../components/UI/Card/Card";
    
      const ErrorPage = () => {
        const navigate = useNavigate();
        // This is the function that will be triggered whenever the button is clicked
        const goHomeHandler = () => {
          navigate("/");
        };
        return (
          <>
            {/* Application */}
            <Card className="nopage__card">
              <h1>Error 404 page!</h1>
              <h2>
                You are seeing this because you are NOT in a valid url. i.e., This
                page does not exist.
              </h2>
              <h3>Kindly go back to a valid url by clicking on the button below</h3>
              <Button onClick={goHomeHandler}>Go home</Button>
            </Card>
          </>
        );
      };
      export default ErrorPage;
    

    useNavigate is a hook from React router library that helps us to navigate dynamically in our application. The hook returns a function that takes the path we want to navigate whenever an event is triggered.

    Below are the Button and Card components that we will be reusing throughout our project.

    Button component

      const Button = (props) => {
        return (
          <button
            onClick={props.onClick}
            type={props.type}
            className={`${props.className} button`}
            disabled={props.disabled}
          >
            {props.children}
          </button>
        );
      };
      export default Button;
    

    Card component

      // A wrapper
      const Card = (props) => {
        return <div className={`${props.className} card`}>{props.children}</div>;
      };
      export default Card;
    
  • ErrorBoundaryPage.jsx: It is in this component that we will test out the Error boundary component that we will be implementing soon.

    The component will contain a logic that will cause an error that will break the app and in which the error will propagate up the parent tree and then be caught by the Error Boundary component that will wrap the whole application.

      import { useState } from "react";
    
      // Already created components
      import Button from "../../components/UI/Button/Button";
      import Card from "../../components/UI/Card/Card";
    
      const ErrorBoundaryPage = (props) => {
        // Managing state
        const [content, setContent] = useState("Joel");
    
        // An error will occur if content data type is set to anything apart from String
        return (
          <>
            {/* Application */}
            <div className="error__boundary--title">Test Error Boundary Page</div>
            <Card className="error__boundary--page">
              <h1>
                Welcome {content.toUpperCase()}, This is the page where you test the
                error boundary.
              </h1>
              <h2>
                When you click the button below, an error will occur which will be
                propagated to the top-most component which is the error boundary
                component and which will then catches the error.
              </h2>
              {/*  The content will be set to an array and so an error will occur*/}
              <Button onClick={() => setContent((prev) => [])}>Start test</Button>
            </Card>
          </>
        );
      };
      export default ErrorBoundaryPage;
    

How to implement Error Boundary in React

There are two ways of writing a React component, either as a class-based component or a functional component.

An error boundary component can only be written as a class-based component and in this project, I am going to make use of the third-party library we installed earlier, the react-error-boundary to implement this feature.

Below are the steps we will follow to implement it

  • Import ErrorBoundary from react-error-boundary.
import { ErrorBoundary } from "react-error-boundary";
  • Create a function that will be passed as the FallbackComponent to the imported component.

      // Error Boundary FallbackComponent: This is the function that will be called whenever the errorboundary component caught an error
      const ErrorFallback = (props) => {
        return (
          <div role="alert" className="boundary__error">
            <p>Something went wrong!</p>
            <pre>{props.error.message}</pre>
            <Button onClick={props.resetErrorBoundary}>Restart app</Button>
          </div>
        );
      };
    
  • Wrap our whole application with the imported component.

import { Navigate, Route, Routes } from "react-router-dom";

const App = () => {
  return (
    <ErrorBoundary
      FallbackComponent={ErrorFallback}
      onReset={() => {
        navigate("/");
      }}
    >
      <Routes>
        <Route path="/" element={<Navigate to="/home" />} />
        <Route path="/home" element={<div>Home page</div>} />
        <Route path="/errorboundary" element={<div>Error Boundary page</div>} />
        {/* Routes that will be matched if none of the Route(s) above is matched */}
        <Route path="*" element={<div>Error 404 page</div>} />
      </Routes>
    </ErrorBoundary>
  );
};
export default App;

Below is App.jsx file latest snapshot.

// Importing the useNavigate hook from react-router-dom .
import { Navigate, Route, Routes, useNavigate } from "react-router-dom";

// Importing the third party error boundary component
import { ErrorBoundary } from "react-error-boundary";

// Importing some of the custom component we had created earlier
import AppHome from "./pages/Home/AppHome";
import ErrorBoundaryPage from "./pages/ErrorBoundaryPage/ErrorBoundaryPage";
import ErrorBoundaryPage from "./pages/ErrorPage/ErrorPage";
import Button from "./components/UI/Button/Button";

// Error Boundary FallbackComponent: This is the function that will be called whenever the errorboundary component caught an error
const ErrorFallback = (props) => {
  return (
    <div role="alert" className="boundary__error">
      <p>Something went wrong!</p>
      <pre>{props.error.message}</pre>
      <Button onClick={props.resetErrorBoundary}>Restart app</Button>
    </div>
  );
};

const App = () => {
  const navigate = useNavigate();
  return (
    // Wrapping the whole application. FallbackComponent will be called whenever an error occured. onReset event to restart the application.
    <ErrorBoundary
      FallbackComponent={ErrorFallback}
      onReset={() => {
        navigate("/");
      }}
    >
      <Routes>
        <Route path="/" element={<Navigate to="/home" />} />

        {/* Nexted routes */}
        <Route path="/home" element={<AppHome />}>
          <Route path="" element={<Home />} />
          <Route path=":id" element={<div>A Repo details</div>} />
        </Route>

        {/* Routes to test the error boundary coponents */}
        <Route path="/errorboundary" element={<ErrorBoundaryPage />} />

        {/* Route to test 404 page */}
        <Route path="/404page" element={<ErrorPage />} />

        {/* Routes that will be matched if none of tthe route(s) is matched */}
        <Route path="*" element={<ErrorPage />} />
      </Routes>
    </ErrorBoundary>
  );
};
export default App;

How to Lazy-Load component in React

Before we go ahead to implement other features or build the remaining components, let me use this medium to explain how to lazy-load our components.

How does lazy loading affect our application?

Lazy loading effectively speeds up our application as it defers loading non-critical components. i.e, a component is loaded only when it is needed.

To lazy load, we need to do the following

  • Import lazy and Suspense from react
import { lazy, Suspense } from "react";
  • Dynamically import the component we want to load lazily.
// Dynamic Imports (Lazy - loading)
const Home = lazy(() => import("./pages/Home/Home"));
const ErrorPage = lazy(() => import("./pages/ErrorPage/ErrorPage"));
  • Wrap our application with the Suspense component imported from React

  • Pass a fallback attribute to the Suspense component as shown below.

<Suspense
  fallback={
    <div className="fallback__box">
      <ImSpinner6 className="fallback__spinner" />
    </div>
  }
>
  <Routes>
    <Route path="/" element={<Navigate to="/home" />} />

    {/* Nexted routes */}
    <Route path="/home" element={<AppHome />}>
      <Route path="" element={<Home />} />
      <Route path=":id" element={<div>A Repo details</div>} />
    </Route>

    {/* Routes to test the error boundary coponents */}
    <Route path="/errorboundary" element={<ErrorBoundaryPage />} />

    {/* Route to test 404 page */}
    <Route path="/404page" element={<ErrorPage />} />

    {/* Routes that will be matched if none of tthe route(s) is matched */}
    <Route path="*" element={<ErrorPage />} />
  </Routes>
</Suspense>

Below is App.jsx latest snapshot

import { lazy, Suspense } from "react";
import { Navigate, Route, Routes, useNavigate } from "react-router-dom";

import { ErrorBoundary } from "react-error-boundary";

import { ImSpinner6 } from "react-icons/im";

import AppHome from "./pages/Home/AppHome";
import ErrorBoundaryPage from "./pages/ErrorBoundaryPage/ErrorBoundaryPage";
import Button from "./components/UI/Button/Button";

// Dynamic Imports (Lazy - loading)
const Home = lazy(() => import("./pages/Home/Home"));
const RepoDetails = lazy(() => import("./components/Repository/RepoDetails"));
const ErrorPage = lazy(() => import("./pages/ErrorPage/ErrorPage"));

// Error Boundary FallbackComponent: This is the function that will be called whenever the errorboundary component caught an error
const ErrorFallback = (props) => {
  return (
    <div role="alert" className="boundary__error">
      <p>Something went wrong!</p>
      <pre>{props.error.message}</pre>
      <Button onClick={props.resetErrorBoundary}>Restart app</Button>
    </div>
  );
};

const App = () => {
  const navigate = useNavigate();

  return (
    <ErrorBoundary
      FallbackComponent={ErrorFallback}
      onReset={() => {
        navigate("/");
      }}
    >
      <Suspense
        fallback={
          <div className="fallback__box">
            <ImSpinner6 className="fallback__spinner" />
          </div>
        }
      >
        <Routes>
          <Route path="/" element={<Navigate to="/home" />} />

          {/* Nexted routes */}
          <Route path="/home" element={<AppHome />}>
            <Route path="" element={<Home />} />
            <Route path=":id" element={<div>A Repo details</div>} />
          </Route>

          {/* Routes to test the error boundary coponents */}
          <Route path="/errorboundary" element={<ErrorBoundaryPage />} />

          {/* Route to test 404 page */}
          <Route path="/404page" element={<ErrorPage />} />

          {/* Routes that will be matched if none of tthe route(s) is matched */}
          <Route path="*" element={<ErrorPage />} />
        </Routes>
      </Suspense>
    </ErrorBoundary>
  );
};
export default App;

Now, what is left of us is to create the remaining components ( Home, Header, Navigation, Pagination and so on), hooks, store (using useContext API) and make use of the GitHub API.

How to make use of useContext API in React

Below are the steps to follow to implement useContext in our project.

  • Create the context using createContext from react and pass in the initial state of the data that will be managed by the context for VS-code auto-completion.
export const DataContext = React.createContext({
  repos: [],
  userDetails: {},
  addReposHandler: () => {},
  addUserHandler: () => {},
});
  • Create a component that will return the context created and call the Provider property on it.

    • Add a value attribute and pass in the data that is to be made available in the whole application
// Creating a component that will provide the context.
const DataContextProvider = (props) => {
  const data = "Data that will be available in all the child components";
  return (
    <DataContext.Provider value={data}>{props.children}</DataContext.Provider>
  );
};
  • Wrap the whole application with the component (index.js file)
<DataContextProvider>
  <BrowserRouter>
    <App />
  </BrowserRouter>
</DataContextProvider>

Below is DataContext.jsx latest snapshot

import React, { useState } from "react";

// This is used to memoize a function
import { useCallback } from "react";

// Creating an app wide state store using the context API
export const DataContext = React.createContext({
  repos: [],
  userDetails: {},
  addReposHandler: () => {},
  addUserHandler: () => {},
});

// Creating a component that will provide the context.
const DataContextProvider = (props) => {
  // Managing states
  const [fetchedRepo, setFetchedRepo] = useState([]);
  const [userDetails, setUserDetails] = useState([]);

  // Functions to updates the states we are . useCallback ensures that the functions are memoized.
  const addReposHandler = useCallback((data) => {
    const formattedData = data.reverse().map((repo, index) => {
      return { ...repo, number: index };
    });
    setFetchedRepo(formattedData);
  }, []);

  const addUserHandler = useCallback((data) => {
    setUserDetails(data);
  }, []);

  // Data that is available in app wide state
  const data = {
    repos: fetchedRepo,
    userDetails: userDetails,
    addReposHandler,
    addUserHandler,
  };

  return (
    <DataContext.Provider value={data}>{props.children}</DataContext.Provider>
  );
};
export default DataContextProvider;

To get access to the data that is provided by the context API in any of the child components, we will do the following.

  • Import useContext hook from react
import { useContext } from "react";
  • Import DataContext (the context created) from the file path
import { DataContext } from "../../store/DataContext";
  • Execute the useContext hook and pass in the context created.

    • Then, you can get any of the data that the context provided. Here we are destructuring the data and getting only the userDetails and then renaming it to user.
const { userDetails: user } = useContext(DataContext);

Below is our final snapshot of UserDetails.jsx file

import { useContext } from "react";
import { DataContext } from "../../store/DataContext";

// This is a custom hook to format date
import useDate from "../../hooks/useDate";

const UserDetails = () => {
  // Consuming the created context as well as Destructuring and given it an alias (renaming from userDetails to user)
  const { userDetails: user } = useContext(DataContext);

  // Formatting the date using the custom function
  const created = useDate(user?.created_at);

  return (
    <div className="user__details">
      <div>
        <h3>Created</h3>
        <h4>{created ? created : null}</h4>
      </div>
      <div>
        <h3>Followers</h3>
        <h2>{user?.followers ?? null}</h2>
      </div>
      <div>
        <h3>Following</h3>
        <h2>{user?.following ?? null}</h2>
      </div>
      <div>
        <h3>Location</h3>
        <h2>{user?.location ?? null}</h2>
      </div>
    </div>
  );
};
export default UserDetails;

How to implement Pagination in react

In this project, we are going to create a pagination component that can be reused all over the application.

The component will receive three (3) props which are,

  • The total number of repositories.

  • The no of repositories per page.

  • A function that will pass the page number to whichever component will make use of the pagination component. (child-to-parent communication)

// A state management hook to manage the page number
import { useState } from "react";

// Pagination buttons
import { BsCircleFill } from "react-icons/bs";
import { ImPrevious, ImNext } from "react-icons/im";

// The reusable pagination component
const Pagination = (props) => {
  // The component will recieve three props
  const { itemPerPage, totalItem, onChange } = props;

  const repoPerPage = itemPerPage;
  const totalRepo = totalItem;

  // Calculating the no of pages
  const total_pages = Math.ceil(totalRepo / repoPerPage);

  // By default we will be seeing page one (1) content
  const [page, setPage] = useState(1);

  // A function handling moving to the previous page
  const prevHandler = () => {
    // If we are on the first page,do  not fnish executing this function
    if (page === 1) return;
    setPage((page) => page - 1);
    onChange(page - 1);
  };

  // A function handling moving to the next page
  const nextHandler = () => {
    // If we are on the last page,do not fnish executing this function
    if (page === total_pages) return;

    setPage((page) => page + 1);
    onChange(page + 1);
  };

  // A function handling changing the page number by clicking any of the pagination icons
  const iconHandler = (num) => {
    onChange(num);
    setPage((page) => num);
  };

  return (
    <div className="pagination__card">
      //Previous button icon
      <ImPrevious
        onClick={prevHandler}
        className={`pagination__icons--prev ${
          page === 1 ? " not__allowed" : ""
        }`}
      />
      //pagination button icon
      <div className="pagination__buttons">
        {Array.from({ length: total_pages }, (_, index) => index + 1).map(
          (each) => (
            <BsCircleFill
              className={`${page === each ? "icon icon__active" : "icon"}`}
              key={each}
              onClick={iconHandler.bind(null, each)}
            />
          )
        )}
      </div>
      //Next button icon
      <ImNext
        onClick={nextHandler}
        className={`pagination__icons--next ${
          page === total_pages || total_pages < 1 ? " not__allowed" : ""
        }`}
      />
    </div>
  );
};
export default Pagination;

Let us go ahead and make use of this pagination component in our application.

Let us create a component that will list out the repositories that will be fetched from GitHub using the GitHub API.

import { useContext, useState } from "react";
import useFetch from "../../hooks/useFetch";
import { DataContext } from "../../store/DataContext";
import Pagination from "../Pagination/Pagination";
import LoadingSpinner from "../UI/LoadingSpinner/LoadingSpinner";
import LeftAlignedRepo from "./LeftRepo";
import RightAlignedRepo from "./RightRepo";

const repoPerPage = 5;

const Repositories = () => {
  // Consuming the created context
  const { repos } = useContext(DataContext);

  // Managing the page rendered
  const [start, setStart] = useState(0);
  const end = start + repoPerPage;

  // A function to update page number that will be passed to the pagination component
  const pageHandler = (page) => {
    setStart((prev) => page * repoPerPage - repoPerPage);
  };

  // Consuming useFetch custom hook
  const { isLoading } = useFetch();

  // API loading state
  if (isLoading) return <LoadingSpinner />;

  return (
    <ul className="repo__box">
      <h2>My repositories</h2>
      {repos.length > 0 ? (
        repos.slice(start, end).map((repo, index) => {
          if (index % 2 === 0) {
            return (
              <LeftAlignedRepo
                key={index}
                id={repo.id}
                index={`${repo.number + 1}`.padStart(2, 0)}
                name={repo.name}
                description={repo.description}
              />
            );
          } else {
            return (
              <RightAlignedRepo
                key={index}
                id={repo.id}
                index={`${repo.number + 1}`.padStart(2, 0)}
                name={repo.name}
                description={repo.description}
              />
            );
          }
        })
      ) : (
        <p className="no__repo">No Repository found</p>
      )}
      {repos.length > 0 && (
        <Pagination
          totalItem={repos.length}
          itemPerPage={repoPerPage}
          onChange={pageHandler}
        />
      )}
    </ul>
  );
};
export default Repositories;

The codes above is the final snapshot of what we will be having in our Repositories.jsx file.

In all of our import statements in the file, it is only the useFetch hook that we are not familiar with and we will be discussing it shortly.

As mentioned earlier, whenever the Pagination component is used, three props are expected.

<Pagination
  totalItem={repos.length}
  itemPerPage={repoPerPage}
  onChange={pageHandler}
/>

pageHandler which is pass to the onChange prop is a function that will get the updated page number from the Pagination component.

// A function to update page number that will be passed to the pagination component
const pageHandler = (page) => {
  setStart((prev) => page * repoPerPage - repoPerPage);
};

So, based on the updated page number from the Pagination component, we will be slicing the array of repositories to display exactly the number of repoPerPage that we had declared in our Repositories.jsx file

Other components that are used in the Repositories.jsx are LeftAlignedRepo, RightAlignedRepo, and LoadingSpinner.

LeftAlignedRepo.jsx file

import { useNavigate } from "react-router-dom";
import Button from "../UI/Button/Button";

const LeftAlignedRepo = (props) => {
  const navigate = useNavigate();

  // A function handling navigation to the repo details page

  const buttonHandler = () => {
    navigate("/home/" + props.id);
  };
  return (
    <li className="repo left__repo">
      <h2>{props.index}</h2>
      <div>
        <h3>{props.name}</h3>
        <p>
          {props.description?.slice(0, 100)}
          <span>...</span>
        </p>
        <Button onClick={buttonHandler}>See more details</Button>
      </div>
    </li>
  );
};
export default LeftAlignedRepo;

RightAlignedRepo.jsx file

import { useNavigate } from "react-router-dom";

import Button from "../UI/Button/Button";

const RightAlignedRepo = (props) => {
  const navigate = useNavigate();

  // A function handling navigation to the repo details page
  const buttonHandler = () => {
    navigate("/home/" + props.id);
  };
  return (
    <li className="repo right__repo">
      <div>
        <h3>{props.name}</h3>
        <p>
          {props.description?.slice(0, 100)}
          <span>...</span>
        </p>
        <Button onClick={buttonHandler}>See more details</Button>
      </div>
      <h2>{props.index}</h2>
    </li>
  );
};
export default RightAlignedRepo;

LoadingSpinner.jsx file

import { ImSpinner10 } from "react-icons/im";
import Card from "../Card/Card";

const LoadingSpinner = () => {
  return (
    <Card className="spinner__box">
      <ImSpinner10 className="spinner__spinner" />
    </Card>
  );
};
export default LoadingSpinner;

How to create a custom hook in react

What is a hook?

A hook is a function that allows one to hook into React state and some other React features**.**

Some hooks such as useState, useEffect, etc., are shipped with React.

We can as well create our custom hook and the following are the things to keep in mind while creating a custom hook.

  • The function name must start with use
const useFetch = () => {};
export default useFetch;
  • You can only use it in the same way you use the hook that comes with react.

  • You can make use of the hooks shipped with React in your custom hook.

To manage our states in the useFetch hook (custom hook), we will be making use of the useReducer hook and not the useState hook.

So, how does the useReducer works?

Check out this link to see how it works.

import { useReducer, useCallback } from "react";

// This is the state initial data
const initialState = {
  isLoading: false,
  error: { hasError: false, message: "" },
};

// This is the function that will be dispatched whenever an action is dispatched.
const fetchReducer = (state, action) => {
  if (action.type === "LOADING") {
    return { ...state, isLoading: action.value };
  }
  if (action.type === "ERROR") {
    return { ...state, error: action.value };
  }
  return initialState;
};

const useFetch = () => {
  // Managing state
  const [fetchState, dispatchFn] = useReducer(fetchReducer, initialState);

  // A function to hide error modal
  const hideModal = () => {
    dispatchFn({ type: "ERROR", value: { hasError: false, message: "" } });
  };

  // A function to fetch data
  const fetchRequest = useCallback(
    async (requestConfig, getRequestData = () => {}) => {
      dispatchFn({ type: "LOADING", value: true });
      dispatchFn({ type: "ERROR", value: { hasError: false, message: "" } });
      try {
        // Fetching data using the configuration provided
        const response = await fetch(requestConfig.url, {
          method: requestConfig.method ? requestConfig.method : "GET",
          body: requestConfig.body ? JSON.stringify(requestConfig.body) : null,
          headers: requestConfig.headers ? requestConfig.headers : {},
        });

        // If the response is not ok, throw an error
        if (!response.ok) {
          throw new Error(`${requestConfig.errorMessage}`);
        }

        // If the response is ok, get the data
        const responseBody = await response.json();

        // Send the data to the function that will use it
        getRequestData(responseBody);
      } catch (err) {
        // If an error occured, set the error state
        dispatchFn({
          type: "ERROR",
          value: { hasError: true, message: err.message || "An error ocurred" },
        });
      }
      // After the request has been made, set the loading state to false
      dispatchFn({ type: "LOADING", value: false });
    },
    []
  );

  // Destcturing the state
  const { isLoading, error } = fetchState;

  // Returning the state and the functions
  return { isLoading, error, hideModal, fetchRequest };
};
export default useFetch;

In the complete useFetch hook file shown above, we can see that the hook returns an object { isLoading, error, hideModal, fetchRequest }. This is the object that we can get access to whenever we execute the useFetch hook.

  • The isLoading state is used to know if the request is still loading or not.

  • The error state is used to know the status of our fetch request.

  • The hideModal is used to clear any error modal that might have popped up if the fetch request failed.

  • The fetchRequest is the function that will receive the GitHub API and send a GET request to it as we will see shortly in the AppHome.jsx file.

Below is the final snaphot of AppHome.jsx file

import { Outlet } from "react-router-dom";
import { useContext, useEffect } from "react";

// Custom hook
import useFetch from "../../hooks/useFetch";
import { DataContext } from "../../store/DataContext";

import Error from "../../components/UI/Error/Error";

const AppHome = () => {
  // Consuming the application wide state data
  const { addReposHandler, addUserHandler } = useContext(DataContext);

  // Consuming the custom hook created
  const { isLoading, error, hideModal, fetchRequest } = useFetch();

  useEffect(() => {
    // This is the function that will get list of repositories from the custom hook and avoid infinite loop.
    const getFetchedData = (data) => {
      addReposHandler(data);
    };

    // This is the function that will get user details from the custom hook and avoid infinite loop.
    const getUserData = (data) => {
      addUserHandler(data);
    };

    fetchRequest(
      {
        url: "https://api.github.com/users/Ojerinde/repos",
        errorMessage: "Could not fetch repositories",
      },
      getFetchedData
    );

    fetchRequest(
      {
        url: "https://api.github.com/users/Ojerinde",
        errorMessage: "Could not fetch user details",
      },
      getUserData
    );
  }, [fetchRequest, addReposHandler, addUserHandler]);

  return (
    <>
      {/* If an error occured while fetching the reposotories */}
      {!isLoading && error.hasError && (
        <Error message={error.message} onClick={() => hideModal()} />
      )}

      {/* This enables the nested route(s) to show */}
      <Outlet />
    </>
  );
};
export default AppHome;

The fetchRequest function from the useFetch hook is configured to receive an object that holds the URL we want to send our request to and a message that is to be displayed if the request failed.

The fetchRequest function also takes in a function that will get the responseBody from the request to avoid an infinite loop.

Other created components

Up until now, we haven't created the component that will render the details of a particular repository whenever we go to "/home/dynamicId" path

RepoDetails.jsx file

  • In the code below we are only not familiar with the useParams hook from React Router.

  • The useParams hooks allow one to access the parameters of the current URL and with this, we can get the selected repository id and then call the find methods on all repositories.

import { useNavigate, useParams } from "react-router-dom";

import { useContext } from "react";
import { DataContext } from "../../store/DataContext";

import { BsBackspaceFill } from "react-icons/bs";

import { Helmet } from "react-helmet-async";

import useDate from "../../hooks/useDate";
import Card from "../UI/Card/Card";

const RepoDetails = () => {
  const navigate = useNavigate();

  // Getting the repo id from the url
  const { id } = useParams();
  // Consuming the created datacontext
  const { repos } = useContext(DataContext);
  // Getting the full object of the id gotten from the URL
  const repo = repos.find((repo) => repo.id === +id);

  // Formatting the data using the useDate function
  const created = useDate(repo.created_at);
  const pushed = useDate(repo.pushed_at);
  const updated = useDate(repo.updated_at);

  return (
    <>
      <Card className="go__home">
        <BsBackspaceFill onClick={() => navigate("/home")} />
        <p>Back</p>
      </Card>
      <section className="repo__full--details">
        <h4>More Details</h4>
        <div>
          <label>Name</label>
          <h1>{repo.name}</h1>
        </div>
        <div>
          <label>ID</label>
          <h5>{repo.id}</h5>
        </div>
        <div>
          <label>Owner</label>
          <h5>{repo.owner.login}</h5>
        </div>
        <div>
          <label>Branch</label>
          <h5>{repo.default_branch}</h5>
        </div>
        <div>
          <label>Visibility</label>
          <h5>{repo.visibility}</h5>
        </div>
        <div>
          <label>Description</label>
          <h5>{repo.description}</h5>
        </div>
        <div>
          <label>Url</label>
          <a href={`${repo.html_url}`} target="_blank" rel="noreferrer">
            {repo.url}
          </a>
        </div>
        <ul>
          <li>
            <label>Created At</label>
            <p>{created ? created : null}</p>
          </li>
          <li>
            <label>Pushed At</label>
            <p>{pushed ? pushed : null}</p>
          </li>
          <li>
            <label>Updated At</label>
            <p>{updated ? updated : null}</p>
          </li>
        </ul>
      </section>
    </>
  );
};
export default RepoDetails;

Header.jsx file

const Header = () => {
  return (
    <header className="home__header">
      <div>
        <h1>
          Hi, I am <span>Joel</span>
        </h1>
        <p>
          I am a Software Engineer <span>and</span> <br /> an Electrical and
          Electronics Engineer.
        </p>
      </div>
    </header>
  );
};
export default Header;

Navigation.jsx file

To navigate from one page to another without causing a page refresh, we are going to import another component which is the Link compnent from react-router-dom

  • The Link component takes the to attribute which also takes the path to the Route we want to navigate to in a quote.
import { Link } from "react-router-dom";

const Navigations = () => {
  return (
    <ul className="navigation__list">
      <li>
        <Link to="/errorboundary">Go to error boundary test page</Link>
      </li>
      <li>
        <Link to="/404page">Go to a page that doesn't exist</Link>
      </li>
    </ul>
  );
};
export default Navigations;

Footer.jsx file

import { ImGithub, ImLinkedin, ImTwitter } from "react-icons/im";

import { Link } from "react-router-dom";

const Footer = () => {
  return (
    <footer className="footer">
      <div>
        <ul className="left">
          <li>
            <Link to="/">Home</Link>
          </li>
          <li>
            <Link to="/errorboundary">Test Error Boundary</Link>
          </li>
          <li>
            <Link to="/404page">Test 404 page</Link>
          </li>
        </ul>
        <ul className="right">
          <li>
            <a
              href="https://github.com/Ojerinde"
              target="_blank"
              rel="noreferrer"
            >
              <ImGithub className="icon icon__git" />
            </a>
          </li>
          <li>
            <a
              href="https://twitter.com/Joel_Ojerinde"
              target="_blank"
              rel="noreferrer"
            >
              <ImTwitter className="icon icon__twit" />
            </a>
          </li>
          <li>
            <a
              href="https://www.linkedin.com/in/ojerinde/"
              target="_blank"
              rel="noreferrer"
            >
              <ImLinkedin className="icon icon__lin" />
            </a>
          </li>
        </ul>
      </div>
      <p>
        ©Ojerinde Joel. You are 100% allowed to used this webpage for both
        personal and commercial use. A credit to the original author is highly
        appreciated.
      </p>
    </footer>
  );
};
export default Footer;

Below is App.jsx final snapshot

import { lazy, Suspense } from "react";
import { Navigate, Route, Routes, useNavigate } from "react-router-dom";

import { ErrorBoundary } from "react-error-boundary";

import { ImSpinner6 } from "react-icons/im";

import AppHome from "./pages/Home/AppHome";
import ErrorBoundaryPage from "./pages/ErrorBoundaryPage/ErrorBoundaryPage";
import Button from "./components/UI/Button/Button";

// Dynamic Imports (Lazy - loading)
const Home = lazy(() => import("./pages/Home/Home"));
const RepoDetails = lazy(() => import("./components/Repository/RepoDetails"));
const ErrorPage = lazy(() => import("./pages/ErrorPage/ErrorPage"));

// Error Boundary FallbackComponent: This is the function that will be called whenever the errorboundary component caught an error
const ErrorFallback = (props) => {
  return (
    <div role="alert" className="boundary__error">
      <p>Something went wrong!</p>
      <pre>{props.error.message}</pre>
      <Button onClick={props.resetErrorBoundary}>Restart app</Button>
    </div>
  );
};

const App = () => {
  const navigate = useNavigate();

  return (
    <ErrorBoundary
      FallbackComponent={ErrorFallback}
      onReset={() => {
        navigate("/");
      }}
    >
      <Suspense
        fallback={
          <div className="fallback__box">
            <ImSpinner6 className="fallback__spinner" />
          </div>
        }
      >
        <Routes>
          <Route path="/" element={<Navigate to="/home" />} />

          {/* Nexted routes */}
          <Route path="/home" element={<AppHome />}>
            <Route path="" element={<Home />} />
            <Route path=":id" element={<RepoDetails />} />
          </Route>

          {/* Routes to test the error boundary coponents */}
          <Route path="/errorboundary" element={<ErrorBoundaryPage />} />

          {/* Route to test 404 page */}
          <Route path="/404page" element={<ErrorPage />} />

          {/* Routes that will be matched if none of tthe route(s) is matched */}
          <Route path="*" element={<ErrorPage />} />
        </Routes>
      </Suspense>
    </ErrorBoundary>
  );
};
export default App;

Conclusion

In this article, we created a React application, implementing various features using third-party libraries such as React-Router, React-Error-Boundry, and React-Icons.

To arrive at the same output, kindly go to the source code and copy all the stylesheets files.

We also fetch our GitHub details using the GitHub API.

Source code: https://github.com/Ojerinde/Github_portfolio

Live Link: https://github-joe.netlify.app/

Thanks for reading!