Create a React Project that consumes GitHub’s API, Implements Error Boundary and Proper SEO

The Confused Creative
13 min readJan 9, 2023
index.js file and folder structure

“Github APIs( or Github ReST APIs) are the APIs that you can use to interact with GitHub. They allow you to create and manage repositories, branches, issues, pull requests, and many more. For fetching publicly available information (like public repositories, user profiles, etc.), you can call the API. For other actions, you need to provide an authenticated token.” Read more.

“Error boundaries are React components that catch JavaScript errors anywhere in their child component tree, log those errors, and display a fallback UI instead of the component tree that crashed.” Read more.

“SEO stands for ‘Search Engine Optimization’, which is the process of getting traffic from free, organic, editorial, or natural search results in search engines. It aims to improve your website’s position in search results pages.” Read more.

In this tutorial, we will be creating a react application that consumes the GitHub API alongside a couple of other features listed below;

- implement an API fetch of your GitHub portfolio,

- show a page with a list of all your repositories on GitHub,

- have pagination implemented for the repo list,

- create another page showing data for a single repo clicked from the list of repos using nested routes,

- implement the proper SEO,

- implement error boundary(show a page to test the error boundary),

- and 404 pages.

Requirements - install the following libraries

Create the necessary folders and files

After creating your react application and installing the libraries listed above, create the different folders and files needed for the project and take out the files we will not be using. Your app structure should look like this when you’re done.

├── node_modules
├── public
├── src
├── ├── assets
├── ├── components
├── ├── ├── 404Page
├── ├── ├── ├── 404Page.css
├── ├── ├── ├── 404Page.jsx
├── ├── ├── Authentication
├── ├── ├── ├── Login.css
├── ├── ├── ├── Login.jsx
├── ├── ├── ├── Signup.jsx
├── ├── ├── Counter
├── ├── ├── ├── Counter.css
├── ├── ├── ├── Counter.jsx
├── ├── ├── ErrorBoundary
├── ├── ├── ├── ErrorBoundary.css
├── ├── ├── ├── ErrorBoundary.jsx
├── ├── ├── Footer
├── ├── ├── ├── Footer.css
├── ├── ├── ├── Footer.jsx
├── ├── ├── Loader
├── ├── ├── ├── Loader.css
├── ├── ├── ├── Loader.jsx
├── ├── ├── RepoData
├── ├── ├── ├── RepoData.css
├── ├── ├── ├── RepoData.jsx
├── ├── ├── RepoList
├── ├── ├── ├── Pagination.jsx
├── ├── ├── ├── RepoCard.jsx
├── ├── ├── ├── RepoHead.jsx
├── ├── ├── ├── RepoList.css
├── ├── ├── ├── RepoList.jsx
├── ├── ├── ├── RepoProfile.jsx
├── ├── pages
├── ├── ├── Home.jsx
├── ├── ├── Data.jsx
├── ├── ├── Error.jsx
├── ├── ├── NoMatch.jsx
├── ├── useContext
├── ├── ├── DataContext.jsx
├── ├── ├── UseData.jsx
├── App.jsx
├── App.css
├── index.js
├── gitignore
├── package.json
├── README.md
└── yarn.lock

Nested route in React

We will be creating the routes for our app in the index.js root file. To do this, import BrowserRouter, Routes, Route from ‘react-router-dom’. We have 5 main routes;

  • the <App/> which contains the RepoList.jsx file routed to path="/" and
  • the <Error/> which tests the ErrorBoundary routed to path="/error".
  • the <Login/> and <Signup/> pages routed to path="/login" and path="/signup" repectively.
  • the <ErrorPage/> which is routed to path=’*’ is used when a user goes to a non-existent route
  • lastly, <Data/> which is routed to path=’/repository/:id’ is a child route of path=”/".

To learn more about nested routes, visit this site.

import React from 'react';
import ReactDOM from 'react-dom/client';
import { BrowserRouter, Routes, Route } from 'react-router-dom';
import App from './App';
import Data from './pages/Data';
import Error from './pages/Error';
import Login from './components/Authentication/Login';
import Signup from './components/Authentication/Signup';
import ErrorPage from './components/404Page/404Page';

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<React.StrictMode>
<BrowserRouter>
<Routes>
<Route path="/" element={<App/>} />
<Route path='*' element={<ErrorPage/>} />
<Route path='/repository/:id' element={<Data/>} />
<Route path='/error' element={<Error/>} />
</Routes>
</BrowserRouter>
</React.StrictMode>
);

*Implement an API fetch of your GitHub portfolio

In our RepoList.jsx will serve as the page with the paginated list of our GitHub repo and GitHub profile, we will be implementing a fetch of our GitHub profile using axios so that it can be passed as props into RepoProfile.jsx and RepoHead.jsx. It has been broken down into smaller components which consist of RepoHead.jsx, RepoProfile.jsx, and RepoCard.jsx.

Now to store this data, create 3 states, and initialize the first state with an empty array. Create another one to check if the data being called is still loading or not, and lastly create another one to check for errors when the API is called.

//RepoList.jsx

import React, { useEffect, useState } from "react";
import Header from "../Header/NavBar";
import axios from "axios";
import RepoCard from "./RepoCard";
import RepoProfile from "./RepoProfile";
import RepoHead from "./RepoHead";
import Footer from "../Footer/Footer";
import "./RepoList.css";

export default function RepositoriesList() {
//state that stores the array of responses from the API
const [profile, setProfile] = useState([]);
//loading state
const [loading, setLoading] = useState(false);
//error state
const [error, setError] = useState("");


// Fetch my github profile
const fetchData = async () => {
const url = "https://api.github.com/users/lilianada";
axios
.get(url)
.then((response) => {
setLoading(true);
setProfile(response.data);
})
.catch((err) => {
setLoading(true);
setError(err);
})
.finally(() => {
setLoading(false);
});
};

useEffect(() => {
fetchData();
}, []);

return (
<main className="mainWrapper">
<Header />
<div className="bodyContent">
<RepoHead profile={profile} />
<div className="content">
<RepoProfile profile={profile} />
<div className="cards">
<RepoCard />
</div>
</div>
</div>
<Footer />
</main>
);
}
//RepoHead.jsx

import React from "react";
import { BiBook } from "react-icons/bi";
import { GoRepo } from "react-icons/go";
import { FiPackage } from "react-icons/fi";
import { AiOutlineLayout } from "react-icons/ai";
import { IoIosStarOutline } from "react-icons/io";
import "./RepoList.css";

export default function RepoHead({profile}) {
return (
<div className="head">
<div className="headItem">
<BiBook className="headIcon" />
<h3>Overview</h3>
</div>
<div className="headItem">
<GoRepo className="headIcon" />
<h3 style={{color: "#58a6ff"}}>
Repositories
<span>{profile.public_repos}</span>
</h3>
</div>
<div className="headItem">
<AiOutlineLayout className="headIcon" />
<h3>Projects</h3>
</div>
<div className="headItem">
<FiPackage className="headIcon" />
<h3>Packages</h3>
</div>
<div className="headItem">
<IoIosStarOutline className="headIcon" />
<h3>
Stars
<span>{profile.public_gists}</span>
</h3>
</div>
</div>
);
}
//RepProfile.jsx
import React from "react";
import { BiBuildings } from "react-icons/bi";
import { FiLink, FiUsers } from "react-icons/fi";
import { BsDot } from "react-icons/bs";
import ErrorBoundary from "../ErrorBoundary/ErrorBoundary";
import "./ReposList.css";

export default function RepoProfile({profile}) {
return (
<ErrorBoundary>
<div className="profile">
<img
src={profile.avatar_url}
alt="Avatar"
className="imageAavatar"
/>
<div className="identity">
<h2 className="profileName">{profile.name} </h2>
<p className="profileLogin">{profile.login} </p>
</div>
<div className="bio">
<p className="bioText">{profile.bio} </p>
</div>
<div className="follows">
<div className="followers">
<FiUsers style={{ marginRight: ".5rem" }} />
<p>
<span className="textBold">{profile.followers} </span>
followers
</p>
</div>
<BsDot fill="#c9d1d9" />
<div className="following">
<p>
<span className="textBold">{profile.following} </span>
following
</p>
</div>
</div>
<div className="locate">
<div className="company">
<BiBuildings style={{ marginRight: ".5rem" }} />
{profile.company && <p> {profile.company} </p>}
</div>
<div className="blog">
<FiLink style={{ marginRight: ".5rem" }} />
{profile.blog && <p> {profile.blog} </p>}
</div>
</div>
</div>
</ErrorBoundary>
);
}

*Show a page with a list of all your repositories on GitHub

Now that we have our Profile displayed on 40% of the RepoList.jsx screen, the other 60% will contain a list of our repositories. To achieve this we will be making use of ContextAPI. We will be using ContextAPI in our app because it’ll make it possible for us to reuse the response from the API call globally throughout our application in any page or component we need it. This way we do not have to do prop drilling which isn’t advisable.

In useContext/DataContext.jsx we will make an API call to our GitHub repositories from GitHub’s page. Mind you, the GitHub API data list is paginated and each page contains 30 items. We will be calling data from the first page.

We imported axios from “axios” and useEffect, useContext, useState from “react”. Create a variable called DataProvider and make your fetch inside of it. Now to store the response from the API, repeat the process we did in the RepoList.jsx page, after which you return DataContext.Provider which holds the props {data, loading, error} that we will be reused throughout the app.

//DataProvider.jsx

import axios from "axios";
import React, { useEffect, useContext, useState } from "react";

export const DataContext = React.createContext();

const DataProvider = (props) => {
//state that stores the array of responses from the API
const [data, setData] = useState([]);
//loading
const [loading, setLoading] = useState(true);
//error
const [error, setError] = useState(null);


const fetchData = async () => {
const url = "https://api.github.com/users/lilianada/repos";
fetch(url);
setLoading(true);
axios
.get(url + "?page={1}+page={2}")
.then((response) => {
setData(response.data);
})
.catch((err) => {
setError(err);
})
.finally(() => {
setLoading(false);
});
};

useEffect(() => {
fetchData();
}, []);

return (
<DataContext.Provider value={{ data, loading, error }}>
{props.children}
</DataContext.Provider>
);
};

export const useDataContext = () => {
const context = useContext(DataContext);
if (!context) {
throw new Error("useData must be used within a DataContext");
}
return context;
};

export default DataProvider;

useDataContext as seen above is used to check for errors that might occur. The DataProvider will be used to wrap the whole application in index.js.

//index.js

import React from "react";
import ReactDOM from "react-dom/client";
import { BrowserRouter, Routes, Route } from "react-router-dom";
import App from "./App";
import Data from "./pages/Data";
import Error from "./pages/Error";
import Login from "./components/Authentication/Login";
import Signup from "./components/Authentication/Signup";
import ErrorPage from "./components/404Page/404Page";
import DataProvider from "./useContext/DataContext";

const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
<React.StrictMode>
<DataProvider>
<BrowserRouter>
<Routes>
<Route index element={<App />} />
<Route path="/repository/:id" element={<Data />} />
<Route path="*" element={<ErrorPage />} />
<Route path="/error" element={<Error />} />
</Routes>
</BrowserRouter>
</DataProvider>
</React.StrictMode>
);

*Paginate RepoList data

To paginate the data response from the API call for GitHub repositories, we will need 4 states,

const [currentPage, setCurrentPage] = useState(1);
const [pageItems, setPageItems] = useState(6);
const [maxPageNum, setMaxPageNum] = useState(6);
const [minPageNum, setMinPageNum] = useState(0);
  1. currentPage — stores current page number, initially 0.
  2. itemsPerPage — stores the number of items we want to display in a single page, initially, it is 6.
  3. maxPageNum — is to store the maximum page-bound limit.
  4. minPageNum — is to store minimum page-bound limit.
const pages = [];
for (let i = 1; i <= Math.ceil(data.length / pageItems); i++) {
pages.push(i);
}

In the above code, the pages array contains a total number of pages. We have 30 items and want to display 6 items per page, then we will need 30/6 = 5 pages.

Now let’s render a component that will display the number of pages. Map the pages array and return its contents as a litag, add an onClick handler that runs when we click on any page number and set the currentPage state to the selected page number, and the className becomes active if the number clicked is the same page as currentPage. The if condition means that if the current number is greater then maxPageNum+1 and less than minPageNum then render it else render nothing.

// component to display the buttons
const showPageNumbers = pages.map((number) => {
if (number < maxPageNum + 1 && number > minPageNum) {
return (
<li
key={number}
id={number}
onClick={handleClick}
className={currentPage == number ? "active" : null}
>
{number}
</li>
);
} else {
return null;
}
)}

Lastly, we need to add the Previous and Next Buttons, and create two variables called handleNextBtn and handlePrevBtn.
In the handleNextBtn method, whenever the user clicks on the next button, it will set the current page state to plus 1 and check the condition if the current page has crossed the maximum page number limit or not. If yes then it will reset this max and min page number limit with the new limit.

For the handlePrevBtn method, the only difference is in the sign and the if condition. It will set the current page state to minus 1. We will disable the previous button when the user is on the first page and disable the next button when the user is on the last page.

//RepoCard.jsx with Pagination 
import React, { useState } from "react";
import { IoIosStarOutline } from "react-icons/io";
import { Link } from "react-router-dom";
import ErrorBoundary from "../ErrorBoundary/ErrorBoundary";
import Pagination from "./Pagination";
import "./RepoList.css";

export default function RepoCard({ data }) {

// stores current page number, initially 1
const [currentPage, setCurrentPage] = useState(1);
// stores no of items we want to display in single page.
const [pageItems, setPageItems] = useState(5);
// maxPageNum and minPageNum are used to set the limit of page numbers to be displayed on the screen.
const [maxPageNum, setMaxPageNum] = useState(5);
const [minPageNum, setMinPageNum] = useState(0);

// Get current items
const indexOfLastItem = currentPage * pageItems;
const indexOfFirstItem = indexOfLastItem - pageItems;
const currentItems = data.slice(indexOfFirstItem, indexOfLastItem);

// ------Pagination
const pages = [];
for (let i = 1; i <= Math.ceil(data.length / pageItems); i++) {
pages.push(i);
}

// onclick handler for the buttons
const handleClick = (event) => {
setCurrentPage(Number(event.target.id));
};

// component to display the buttons
const showPageNumbers = pages.map((number) => {
//if current number is greater than maxPageNum +1 and less than minPageNum then render it else render nothing.
if (number < maxPageNum + 1 && number > minPageNum) {
return (
<li
key={number}
id={number}
onClick={handleClick}
className={currentPage === number ? "active" : null}
>
{number}
</li>
);
} else {
return null;
}
});

// Next and Previous button handlers
const handleNextBtn = () => {
setCurrentPage(currentPage + 1);
//if current page +1 is greater than maxPageNum then set maxPageNum and minPageNum to 9 and 0 respectively.
if (currentPage + 1 > maxPageNum) {
setMaxPageNum(maxPageNum + pageItems);
setMinPageNum(minPageNum + pageItems);
}
};

const handlePrevBtn = () => {
setCurrentPage(currentPage - 1);
//if current page -1 is 0 then set maxPageNum and minPageNum to 9 and 0 respectively.
if ((currentPage - 1) % pageItems === 0) {
setMaxPageNum(maxPageNum - pageItems);
setMinPageNum(minPageNum - pageItems);
}
};

return (
<ErrorBoundary>
{currentItems.map((item) => {
return (
<div className="card" key={item.id}>
<div className="repoLink">
<h5 className="repoName"> {item.name} </h5>
<div className="repoStatus">
<p> {item.visibility} </p>
</div>
</div>
<div className="description">
<p> {item.description} </p>
</div>
<div className="row">
<div className="language">
<p>{item.language}</p>
</div>
<div className={`row ${item.stargazers_count === 0 ? "none" : ""}`}>
<IoIosStarOutline
fill="#8b949e"
style={{ marginRight: ".5rem" }}
/>
<p>{item.stargazers_count}</p>
</div>
</div>
<Link to={"/repository/" + item.id} className="viewRepo">
View Repo
</Link>
</div>
);
})}

{/* Pagination */}
<Pagination
handlePrevBtn={handlePrevBtn}
handleNextBtn={handleNextBtn}
showPageNumbers={showPageNumbers}
currentPage={currentPage}
pages={pages}
/>
</ErrorBoundary>
);
}

Pass handlePrevBtn, handleNextBtn, showPageNumbers, currentPage as props to the Pagination component. With this Pagination is complete and the repositories are displayed.

import React from "react";
import ErrorBoundary from "../ErrorBoundary/ErrorBoundary";
import "./RepoList.css";

export default function Pagination({
handlePrevBtn,
handleNextBtn,
showPageNumbers,
currentPage,
pages,
}) {
return (
<ErrorBoundary>
<div className="pagination">
<ul className="pageNumbers">
<li>
<button
onClick={handlePrevBtn}
disabled={currentPage === pages[0] ? true : false}
className={currentPage !== pages[0] ? "active" : null}
>
Prev
</button>
</li>
{showPageNumbers}
<li>
<button
onClick={handleNextBtn}
disabled={currentPage === pages[pages.length - 1] ? true : false}
className={currentPage !== pages.length ? "active" : null}
>
Next
</button>
</li>
</ul>
</div>
</ErrorBoundary>
);
}

*Create another page showing data for a single repo clicked from the list of repos using nested routes

In useContext/UseData, we will filter through data by repo id and return details about the particular repo clicked on in RepoData.jsx. First import useDataContext from ”./DataContext” , call data from useDataContext and filter through it to get the item clicked. The details should display in “/repository/:id” page.

import { useDataContext } from "./DataContext";

const UseData = () => {
const { data } = useDataContext();

const getData = (id) => {
const items = data.filter((item) => parseInt(item.id) === parseInt(id));
return items[0];
};
return { getData };
};
export default UseData;

*Lazy loading in React and Loading Animation

Lazy loading is a technique that enables us to load a specific component when a particular route is accessed. As the name says the main purpose of this component is to lazily load the mentioned component which enhances the load time and the loading speed. At the same time, it increases the react application performance.

To lazy load our current app, do the following;

  • Import lazy and Suspense from react in the index.js
  • Dynamically import the component we want to load lazily.
const Home = lazy(() => import("./App"));
const RepoData = lazy(() => import("./pages/Data"));
const NoMatch = lazy(() => import("./pages/NoMatch"));
const ErrorBoundary = lazy(() => import("./pages/Error"));
const Login = lazy(() => import("././components/Authentication/Login"));
const Signup = lazy(() => import("./components/Authentication/SignuP"));
  • Wrap our application with the Suspense component imported from React
  • Pass a fallback attribute to the Suspense component as shown below.

In the index.js file, the Suspense fallback should be the <Loader/>. Now when you refresh your page, you’ll see the loader before the page content.

import React, {lazy, Suspense} from "react";
import ReactDOM from "react-dom/client";
import { HelmetProvider } from "react-helmet-async";
import { BrowserRouter, Routes, Route } from "react-router-dom";
import DataProvider from "./useContext/DataContext";
import Loader from "./components/Loader/Loader";

//Dynamic imports
const Home = lazy(() => import("./App"));
const RepoData = lazy(() => import("./pages/Data"));
const NoMatch = lazy(() => import("./pages/NoMatch"));
const ErrorBoundary = lazy(() => import("./pages/Error"));
const Login = lazy(() => import("././components/Authentication/Login"));
const Signup = lazy(() => import("./components/Authentication/SignuP"));

const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
<React.StrictMode>
<HelmetProvider>
<DataProvider>
<Suspense fallback={<Loader/>}>
<BrowserRouter>
<Routes>
<Route index element={<Home />} />
<Route path="/repository/:id" element={<RepoData />} />
<Route path="*" element={<NoMatch />} />
<Route path="/error" element={<ErrorBoundary />} />
<Route path="/login" element={<Login />} />
<Route path="/signup" element={<Signup />} />
</Routes>
</BrowserRouter>
</Suspense>
</DataProvider>
</HelmetProvider>
</React.StrictMode>
);

To add loading animation to this project, first, create a Loader.jsx file inside of the Loader folder. The <Loader />is made of a simple css spinner which you can style however you want or you can choose to use a library. Your code should look like this.

import React from "react";
import "./Loader.css";
export default function Loader() {
return (
<div className="loaderContainer">
<div className="spinner"></div>
</div>
);
}

*Implement proper SEO

To implement proper SEO for your react application, I suggest you watch this video and follow it step by step.

*Implement Error boundary (show a page to test the error boundary)

For the implementation of the Error boundary, see the articles below after which you create a page and create a simple react counter. Break the counter so that it can throw an error to show that the error boundary works.

*404 page

For the 404 page, create a simple page that tells the user that the page they have been routed to doesn’t exist. You can add a button that takes them back to the previous page or to the homepage. You can add images from error404.fun to spice it up.

Conclusion

Every external article or video linked to this project played a major role in the result you see above, I believe those writers and YouTubers did a better job at explaining those different concepts better than I would. So take your time to go through them as they will aid you in achieving the same result as I have.

If you have any questions, you can leave them in the comments section below. Thank you for reading and I appreciate your feedback.

See the completed demo here.

See the source code here.

--

--

The Confused Creative

Multifaceted designer. Frontend Dev. Writer. Artist. Jane of all trades.