How I created a web page that displays all my GitHub repositories with ReactJs
Table of contents
This article shows how I created a react app that displays all my GitHub repositories. The main purpose of this article is to explain how the web app was built using the following features of Reactjs:
UseState
UseEffect
React router
Nested routes
404 Error page
Error boundaries
Error boundaries using class components
React error boundaries.
## How does the app work?
The web app has the "Home" and "Repositories" navigation tabs. The list of repositories is displayed when the "Repositories" tab is clicked.
- Each repository has a "view more" button that displays more detail about the repository when clicked.
Implementation in Reactjs
- Install react in a folder using "create-react-app", navigate to the folder, open the folder on Vscode and delete unwanted default files and code blocks.
npm install create-react-app gitrepoapp
cd gitrepoapp
code .
Create a "component" folder and a "pages" folder inside the "src" folder.
In the component folder, create a Navigation.js component. This component has two unordered list (li) elements; "Home" and "Repositories".
import React from "react";
import "./Navigation.css";
const Navigation = () => {
return (
<>
<div className="navigation">
<header>
<p>GITHUB</p>
<p>REPO</p>
</header>
<ul className="nav-list">
<li>
Home
</NavLink>
</li>
<li>
Repositories
</li>
</ul>
<footer>©Prius2055 2022</footer>
</div>
</>
);
};
export default Navigation;
In the pages folder, create a Home.js component and a GitRepoList.js component.
Home.js
import React from "react"; import "./Home.css"; const Home = () => { return ( <React.Fragment> <div className="home-container"> <h1> Welcome <br></br>to my Github Repository page </h1> <h4> To view a list of my github repositories, click on the repository list in the navigation bar </h4> </div> </React.Fragment> ); }; export default Home;
The GitRepoList component uses useEffect to make an API call to the github user database, the result of the API call is then stored in a state using the useState hook, then the javascript map method is used to display the required details of individual repositories to be displayed in the UI.
GitRepoList.Js
import React, { useEffect, useState } from "react";
import "./GitRepoList.css";
const GitRepoList = (props) => {
const [repos, setRepos] = useState(null);
useEffect(() => {
fetch("https://api.github.com/users/prius2055/repos")
.then((res) => res.json())
.then((data) => setRepos(data));
}, []);
return (
<>
<div className="repo-list">
<h1>My repository list</h1>
<div className="repo-card-container">
{repos.map((repo, i) => {
return (
<div className="repo-card" key={repo.id}>
<h2>{repo.name}</h2>
<button className="repo-btn">
View more
</button>
</div>
);
})}
</div>
</>
);
};
export default GitRepoList
- Pagination. Here we determine the number of repositories we want to be displayed in the UI per page. To do this, we use the client-side pagination method. This is when the developer computes and implements the pagination, not allowing it to be computed from the database structure.
PER_PAGE: How many items do we want to display on a page
pages: Total number of pages the UI should contain
skip: A number used to determine, how the slice method should be implemented with the array of repositories.
import React, { useEffect, useState } from "react";
import "./GitRepoList.css";
const GitRepoList = (props) => {
const [repos, setRepos] = useState(null);
const [page, setPage] = useState(1);
const navigate = useNavigate();
useEffect(() => {
fetch("https://api.github.com/users/prius2055/repos")
.then((res) => res.json())
.then((data) => setRepos(data));
}, []);
const PER_PAGE = 3;
const pages = Math.ceil(repos?.length / 3);
const skip = page * PER_PAGE - PER_PAGE;
const prevHandler = (e) => {
e.preventDefault();
if (page !== 1) {
setPage((prev) => prev - 1);
} else {
return;
}
};
const nextHandler = (e) => {
e.preventDefault();
if (page !== pages) {
setPage((prev) => prev + 1);
} else {
return;
}
};
const navButtonHandler = (e) => {
e.preventDefault();
setPage(Number(e.target.innerText));
};
const getRepoDetail = (repo) => {
props.repoDetailUpdate(repo);
};
return (
<>
<div className="repo-list">
<h1>My repository list</h1>
<div className="repo-card-container">
{!repos && <div>Loading Repositories, please wait</div>}
{repos?.slice(skip, skip + PER_PAGE).map((repo, i) => {
return (
<div className="repo-card" key={repo.id}>
<h2>{repo.name}</h2>
<button className="repo-btn">
View more
</button>
</div>
);
})}
</div>
<div className="list-btns">
{repos?.slice(0, pages).map((_, index) => (
<button className="list-btn-num" onClick={navButtonHandler}>
{index + 1}
</button>
))}
<div className="list-btn-nav">
<button onClick={prevHandler}>Prev</button>
Pages {page} of {pages}
<button onClick={nextHandler}>Next</button>
</div>
</div>
</div>
</>
);
};
export default GitRepoList;
Implementing React router
The React router is implemented in the App.js component.
First by installing react-router-dom@v6 and wrapping the App.js component in <BrowserRouter>, after importing it. This can be done in the index.js component. Then, in the Navigation component, we wrap The "Home" and "Repositories" components inside <NavLink> after importing it.
npm install react-router-dom@v6
index.js
import React from "react";
import ReactDOM from "react-dom/client";
import { BrowserRouter } from "react-router-dom";
import "./index.css";
import App from "./App";
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
<React.StrictMode>
<BrowserRouter>
<App />
</BrowserRouter>
</React.StrictMode>
);
Inside the App.js component, we wrap all the components displayed here in <Routes>
We also create two more components;
GitRepoDetail.js and ErrorPage.js
import { useStat} from "react"; import { Routes, Route } from "react-router-dom"; import "./App.css"; import Home from "./component/pages/Home"; import Navigation from "./component/Navigation"; import GitRepoList from "./component/pages/GitRepoList"; import GitRepoDetail from "./component/pages/GitRepoDetail"; import ErrorPage from "./component/pages/ErrorPage"; function App() { return ( <ErrorBoundary fallback={ErrorFallback}> <div className="App"> <Navigation /> <div className="app-page"> <Routes> <Route path="/" element={<Home />} /> <Route path="/repo-list" element={<GitRepoList/>}> <Route path=":repoId" element={<GitRepoDetail/>}/> </Route> <Route path="*" element={<ErrorPage />} /> </Routes> </div> </div> ); } export default App;
Navigation.js
import React from "react"; import { NavLink } from "react-router-dom"; import "./Navigation.css"; const Navigation = () => { return ( <> <div className="navigation"> <header> <p>GITHUB</p> <p>REPO</p> </header> <ul className="nav-list"> <li> <NavLink to="/" style={({ isActive }) => ({ backgroundColor: isActive ? "#bb3e03" : "", textDecoration: "none", display:'block', color: isActive ? "#fff" : "", padding: isActive ? "20px" : "", })} > Home </NavLink> </li> <li> <NavLink to="/repo-list" style={({ isActive }) => ({ backgroundColor: isActive ? "#bb3e03 " : "", textDecoration: "none", display:'block', color: isActive ? "#fff" : "", padding: isActive ? "20px" : "", })} > Repositories </NavLink> </li> </ul> <footer>©Prius2055 2022</footer> </div> </> ); }; export default Navigation;
GitRepoDetail.js
import React from "react";
import "./GitRepoDetail.css";
import { AiOutlineFork, AiTwotoneUnlock, AiTwotoneLock } from "react-icons/ai";
import { Link } from "react-router-dom";
const GitRepoDetail = (props) => {
return (
<>
<div className="repo-detail">
<h2>Repository detail</h2>
<div className="repo-detail-block">
<div className="detail-name">
<p>Name of Repository: {props.currentRepo.name}</p>
<p>
{props.currentRepo.visibility === "public" ? (
<AiTwotoneUnlock />
) : (
<AiTwotoneLock />
)}
</p>
</div>
<p>Languages used in Repository: {props.currentRepo.language}</p>
<p>
<AiOutlineFork />
{props.currentRepo.forks}
</p>
<p>
<a
href={props.currentRepo.html_url}
target="_blank"
>
click here to view github repository
</a>
</p>
<p>
<a
href={`https://prius2055.github.io/${props.currentRepo.name}`}
target="_blank"
>
click here to visit webpage
</a>
</p>
</div>
</div>
</>
);
};
export default GitRepoDetail;
404 Error page: This page is displayed whenever a user searches for a non-existing page.
ErrorPage.js
import React from "react";
import './ErrorPage.css'
const ErrorPage = () => {
return (
<div className="error-page">
<h1>404 Error</h1>
<p>This page does not exist. Please click to the Home button to go back</p>
</div>
);
};
export default ErrorPage;
Implementing Nested routes
Nested routes are routes within routes. In our case, clicking on the "view button" located in the GitRepoList component displays the content of the GitRepoDetail component. So the GitRepoDetail component is nested inside the GitRepoList component. Inside the GitRepoDetail component, we add the useParam hook, which ensures that we are able to navigate to a specific repository when clicked.
GitRepoDetail.js
import React from "react";
import { useParams } from "react-router";
import "./GitRepoDetail.css";
import { AiOutlineFork, AiTwotoneUnlock, AiTwotoneLock } from "react-icons/ai";
import { Link } from "react-router-dom";
const GitRepoDetail = (props) => {
const { repoId } = useParams();
return (
<>
<div className="repo-detail">
<h2>Repository detail</h2>
<div className="repo-detail-block">
<div className="detail-name">
<p>Name of Repository: {props.currentRepo.name}</p>
<p>
{props.currentRepo.visibility === "public" ? (
<AiTwotoneUnlock />
) : (
<AiTwotoneLock />
)}
</p>
</div>
<p>Languages used in Repository: {props.currentRepo.language}</p>
<p>
<AiOutlineFork />
{props.currentRepo.forks}
</p>
<p>
<a
href={props.currentRepo.html_url}
target="_blank"
>
click here to view github repository
</a>
</p>
<p>
<a
href={`https://prius2055.github.io/${props.currentRepo.name}`}
target="_blank"
>
click here to visit webpage
</a>
</p>
</div>
</div>
</>
);
};
export default GitRepoDetail;
We also add the useNavigate hook and <Outlet> to the component nesting the GitRepoDetail component
GitRepoList.js
import React, { useEffect, useState } from "react";
import { Outlet, useNavigate } from "react-router-dom";
import "./GitRepoList.css";
const GitRepoList = (props) => {
const [repos, setRepos] = useState(null);
const [page, setPage] = useState(1);
const navigate = useNavigate();
useEffect(() => {
fetch("https://api.github.com/users/prius2055/repos")
.then((res) => res.json())
.then((data) => setRepos(data));
}, []);
// console.log(repos);
const PER_PAGE = 3;
const pages = Math.ceil(repos?.length / 3);
const skip = page * PER_PAGE - PER_PAGE;
const prevHandler = (e) => {
e.preventDefault();
if (page !== 1) {
setPage((prev) => prev - 1);
} else {
return;
}
};
const nextHandler = (e) => {
e.preventDefault();
if (page !== pages) {
setPage((prev) => prev + 1);
} else {
return;
}
};
const navButtonHandler = (e) => {
e.preventDefault();
setPage(Number(e.target.innerText));
};
const getRepoDetail = (repo) => {
props.repoDetailUpdate(repo);
};
return (
<>
<div className="repo-list">
<h1>My repository list</h1>
<div className="repo-card-container">
{!repos && <div>Loading Repositories, please wait</div>}
{repos?.slice(skip, skip + PER_PAGE).map((repo, i) => {
return (
<div className="repo-card" key={repo.id}>
<h2>{repo.name}</h2>
<button
className="repo-btn"
onClick={(e) => {
e.preventDefault();
getRepoDetail(repo);
navigate(`/repo-list/${repo.id}`);
}}
>
View more
</button>
</div>
);
})}
</div>
<div className="list-btns">
{repos?.slice(0, pages).map((_, index) => (
<button className="list-btn-num" onClick={navButtonHandler}>
{index + 1}
</button>
))}
<div className="list-btn-nav">
<button onClick={prevHandler}>Prev</button>
Pages {page} of {pages}
<button onClick={nextHandler}>Next</button>
</div>
</div>
<Outlet />
</div>
</>
);
};
export default GitRepoList;
- Error boundaries and ReactJs SEO best practice using lazy loading
Error boundaries are used in react to render an alternative UI in case of a Javascript runtime error. It can be implemented in two ways; using error boundary class component and by installing a dependency using npm install react-error-boundary. After which we import ErrorBoundary in our App component, and wrap all the children inside the App component within the <ErrorBoundary>. The <ErrorBoundary> component has a child element called <Suspense> which has a fallback prop that is called into action whenever an error occurs within the App.js component. This fallback prop can have any element from an HTML div to a spinner.
<ErrorBoundary fallback={ErrorFallback}>
<Suspense
fallback={
<div className="fallback">
<CirclesWithBar
height="100"
width="100"
color="#4fa94d"
wrapperStyle={{}}
wrapperClass=""
visible={true}
outerCircleColor=""
innerCircleColor=""
barColor=""
ariaLabel="circles-with-bar-loading"
/>
<h2>Please wait, while your page loads</h2>
</div>
}
>
<div className="App">
<Navigation />
<div className="app-page">
<Routes>
<Route path="/" element={<Home />} />
<Route
path="/repo-list"
element={<GitRepoList repoDetailUpdate={repoDetailHandler}/>}
>
<Route
path=":repoId"
element={<GitRepoDetail currentRepo={currentRepo} />}
/>
</Route>
<Route path="*" element={<ErrorPage />} />
</Routes>
</div>
</div>
</Suspense>
</ErrorBoundary>
To implement SEO for this React app we use Lazy loading, which ensures only required components are loaded when the app mounts. This prevents the app from loading all the components it was built with whenever the app mounts and ensures only the component to be displayed in the UI is loaded per time.
App.js
import { useState, lazy, Suspense } from "react";
import { Routes, Route } from "react-router-dom";
import { CirclesWithBar } from "react-loader-spinner";
import { ErrorBoundary } from "react-error-boundary";
import ErrorFallback from "./component/ErrorFallback";
import "./App.css";
const Home = lazy(() => import("./component/pages/Home"));
const Navigation = lazy(() => import("./component/Navigation"));
const GitRepoList = lazy(() => import("./component/pages/GitRepoList"));
const GitRepoDetail = lazy(() => import("./component/pages/GitRepoDetail"));
const ErrorPage = lazy(() => import("./component/pages/ErrorPage"));
function App() {
const [currentRepo, setCurrentRepo] = useState();
const repoDetailHandler = (repo) => {
setCurrentRepo(repo);
};
return (
<ErrorBoundary fallback={ErrorFallback}>
<Suspense
fallback={
<div className="fallback">
<CirclesWithBar
height="100"
width="100"
color="#4fa94d"
wrapperStyle={{}}
wrapperClass=""
visible={true}
outerCircleColor=""
innerCircleColor=""
barColor=""
ariaLabel="circles-with-bar-loading"
/>
<h2>Please wait, while your page loads</h2>
</div>
}
>
<div className="App">
<Navigation />
<div className="app-page">
<Routes>
<Route path="/" element={<Home />} />
<Route
path="/repo-list"
element={<GitRepoList repoDetailUpdate={repoDetailHandler} />}
>
<Route
path=":repoId"
element={<GitRepoDetail currentRepo={currentRepo} />}
/>
</Route>
<Route path="*" element={<ErrorPage />} />
</Routes>
</div>
</div>
</Suspense>
</ErrorBoundary>
);
}
export default App;
CSS:
(1) Navigation.css
.navigation {
background-color: #ccc;
width: 15%;
height: 100vh;
text-align: center;
padding-top: 1%;
padding-bottom: 3%;
display: flex;
flex-direction: column;
justify-content: space-between;
font-size: 16px;
font-weight: 600;
}
.navigation header {
background-color: #fff;
align-self: center;
width: 90px;
height: 80px;
border-radius: 50% 50%;
padding: 15px;
}
.nav-list {
text-transform: uppercase;
}
.nav-list li {
margin-bottom: 50px;
}
(2) ErrorPage.css
.error-page {
top: 50%;
left: 60%;
transform: translate(-50%, -50%);
}
(3) GitRepoDetail.css
.repo-detail {
margin-top: 20px;
text-align: center;
width: 100%;
height: 100%;
padding-top: 2%;
border-radius: 12px;
box-shadow: 0px 1px 6px 0px rgba(0, 0, 0, 0.82);
}
.repo-detail-block {
padding-top: 1%;
}
.repo-detail-block p{
padding-top: 10px;
}
(4) GitRepoList.css
.repo-list {
display: flex;
flex-direction: column;
max-width: 100%;
margin: 0 auto;
height: 100vh;
padding: 3%;
}
.repo-list h1 {
margin-bottom: 100px;
}
.repo-card-container {
display: flex;
justify-content: space-evenly;
}
.repo-card {
border-radius: 12px;
margin-top: 20px;
text-align: center;
width: 450px;
height: 200px;
padding-top: 2%;
box-shadow: 0px 1px 6px 0px rgba(0,0,0,0.82);
}
.repo-card button{
cursor: pointer;
}
.repo-btn {
padding: 10px;
background-color: #bb3e03;
border: 1px solid #bb3e03;
color: #fff;
margin-top: 50px;
border-radius: 5px;
font-size: 14px;
}
.list-btns {
margin-top: 50px;
text-align: center;
}
.list-btn-num {
padding: 10px 21px;
margin-bottom: 15px;
cursor: pointer;
}
.list-btn-nav button {
width: 100px;
height: 50px;
margin-right: 20px;
margin-left: 20px;
cursor: pointer;
}
(5) Home.css
.home-container {
color: #ccc;
align-self: center;
position: absolute;
top: 50%;
left: 60%;
transform: translate(-50%, -50%);
}
.home-container h1 {
/* background-color: rgb(38, 38, 38); */
background-color: #bb3e03;
text-align: left;
text-transform: uppercase;
padding: 10px;
margin-bottom: 50px;
}
.home-container h4 {
color: rgb(38, 38, 38);
font-weight: 600;
line-height: 20px;
}
(6) App.css
.App {
padding: 0;
margin: 0;
display: flex;
justify-content: center;
color: #333333;
}
.app-page {
width: 85%;
min-height: 100vh;
}
.fallback{
top: 50%;
left: 60%;
transform: translate(-50%, -50%);
}
Conclusion
Building this App involved implementing some basic Reactjs features. You can always follow the procedures to build similar apps. Thank you.
Subscribe to my newsletter
Read articles from Prince directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
Prince
Prince
I am a frontend developer from Nigeria, currently enrolled at Altschool Africa for a software Engineering degree. I love to learn, grow, collaborate and help others become better