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
, andNavigate
as a named import fromreact-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
andelement
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 theAppHome
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 touser
.
- Then, you can get any of the data that the context provided. Here we are destructuring the data and getting only the
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/