Part 1: The Basics
So, it was the end of my second semester at AltSchool Africa and I had an exam project to create a site showing my github profile and details about my repositories. In this article, I will explain how I went about implementing this project.
Tooling
As far as tooling goes, this project was made using Replit, which is an online IDE and I did this so I could work anywhere and on any device on my project. Replit comes with React tooling already set up (via Vite) so you can set up a new React project in seconds and install packages using the GUI.
Structure
All of the code was initially written in a single App component to make it so that I could get a working prototype as quickly as possible. All refactoring will be done in a more advanced portion of this article.
Code
I identified the most important steps to get my project up and running to be:
- Fetching my account from the github API using the Fetch API.
- Creating some basic state to assign the fetch results to.
Fetching data from an API and setting that data to a state (using a setter function) will often reuquire the use of the useEffect
hook. This is done in order to avoid an infinite re-rendering cycle in React as the JSON objects fetched cannot be read as the same result even if they contain the exact same content.
I hence created an asynchronus getUser()
function to handle all my API calls and promises. In that function, I implemented the fetch as follows:
const totalRepos= await fetch(`https://api.github.com/users/eyesaidyo`)
.then(res=>res.json())
.then(data=>data.public_repos)
Creating state to assign the data to went as follows:
const [currentPage, setCurrentPage]= useState('')
This state is what will be used to track and change paginated data coming from the API.
A second fetch was then implemented in the getUser function this time, to get the actual repositories and their data:
fetch(`https://api.github.com/users/eyesaidyo/repos?per_page=${reposPerPage}&page=${currentPage}`)
.then(res=>res.json())
.then(res=>{
toSetRepos(res)
// console.log(res)
toSetPageCount(Math.ceil(totalRepos/reposPerPage))
})
toSetRepos
is a setter function that gets a repos array for whatever the current page searched from the API is.
toSetPageCount
calculates the number of pages needed to accomodate all the repos in my profile based on a fixed reposPerPage
variable.
To make sure that I am able to get a certain list of repos based on which page in the fetch request is specified(currentPage
):
useEffect(()=>{
getUser()
}, [currentPage])
Now, whenever the component is mounted or there is a change to currentPage
via a setter function, my getUser
function runs.
In order to display the repos, a Cards component was made:
export const Cards=({repos})=>{
return (
<div>
{repos.map((repo)=>{
return(
<Link className='repo-item-link' to={repo.name} key={repo.id}>
<h3 className='repo-item' >{repo.name}</h3>
</Link>
)
})}
</div>
)
}
This way, each repo item on the currentPage
will be rendered as Link
components which link to a new pathway, repo.name
that will in turn show more specific details partaining to the particular repository.
Routing
The first thing is to import Routes
and Route
from react-router-dom
Routes
acts as a container for each individual Route
and what it links to
.Note that Route
needs a path
property and an element
property. path
specifies the directory the webpage goes to in relation to its homepage or parent page. element
specifies a component to be displayed based on what directory the webpage is in. An example is as follows:
<Routes>
<Route path='/' element={<Nav/>}>
<Route index element={<Home/>}/>
</Route>
<Routes>
I created a <Route>
for each repository Link
(existing in the Cards
component) by using the map
function and looping through the state that is repos
:
{
repos.map(repo=>{
return(
<Route path={`/${repo.name}`} key={repo.id} element={<Card
name={repo.name}
forks={repo.forks}
visibility={repo.visibility}
createdAt={repo.created_at}
language={repo.language} watchers={repo.watchers}
url={repo.html_url}
/>}>
</Route>
)
})}
A route was also created to show a <NotFound/>
component anytime the user goes to a wrong directory (*
)
<Route path='*' element={<NotFound/>}> </Route>
Pagination
As things stand currently, this webpage can only display one small piece of the data fetched from the API at a time, This is because the fetch is paginated and only serves a few repos at a time (reposPerPage
) on whatever the currentPage
is.
We need a way to dynamically set the currentPage to whatever we want; this is where pagination comes in .
In this project, I created my own basic pagination without any external libraries. My general idea was to use the calculated pageCount
gotten from the following snippet in the getUser
function: toSetPageCount(Math.ceil(totalRepos/reposPerPage))
; this pageCount
lets me know the total number of pages I'd need to accomodate all the repositories in my profile. I would then generate a button representing each page and assign an onClick()
which calls the setCurrentPage
function. Of course, showing every single button at the same time could make the UI bloated so I decided to show only 5 buttons at a time and then make a 'prev' and 'next' button to show the previous or the next 5 buttons if any. This previous and next buttons were set to disable at the beginning and at the end of available pages.
The one thing missing is a way to map through all the pages since the pageCount
is not an array. I generated the array using a for
loop as follows:
const getPagesArray=()=>{
let ans=[];
for(let i=1; i<=pageCount; i++){
ans.push(i)
}
return ans;
}
const pagesArray=getPagesArray()
Now armed with my pagesArray
, I can choose to display only a slice()
of it in order to show only buttonsPerPage
number of buttons at a time (mine is set to five).
There is also state for firstIndex
and lastIndex
so I can use the prev and next buttons to show the next 5 and previous 5 buttons. This is how the code was written:
{
pagesArray.slice(firstIndex, lastIndex).map((num,ind)=>
{
return (
<button
key={ind}
onClick={()=>{
toSetCurrentPage(pagesArray.indexOf(num)+1)
}}
>{pagesArray.indexOf(num)+1}</button>)
}
)
}
The prev and next buttons were written like this:
<button
className='pg-btn'
disabled={firstIndex===0}
onClick={()=>{
toSetFirstIndex(firstIndex-buttonsPerPage+1 >=0?
firstIndex-buttonsPerPage:
null)
toSetLastIndex(lastIndex=== pageCount?
lastIndex-(pageCount%buttonsPerPage):lastIndex-buttonsPerPage
)
}}
>prev</button>
<button
className='pg-btn'
disabled={lastIndex===pageCount || pageCount<buttonsPerPage}
onClick={()=>{
toSetFirstIndex(firstIndex+buttonsPerPage)
toSetLastIndex(lastIndex+buttonsPerPage <=pageCount?lastIndex+buttonsPerPage:pageCount)
}}
>
next
</button>
Part 2: Refactoring and Improvements
The 2 main areas I wanted to refactor and improve were :
-Change the pagination components state manager from useState
to useReducer
-Move the pagination component out of the App
component and make the state available to the App
component using the Context API
It started up as follows:
const INITIAL_STATE={
pageCount:0,
firstIndex:0,
lastIndex:0,
currentPage:1,
repos:[]
}
export const PaginationContext=createContext({
...INITIAL_STATE,
toSetFirstIndex:()=>{},
toSetLastIndex:()=>{},
toSetCurrentPage:()=>{},
toSetPageCount:()=>{},
toSetRepos:()=>{}
})
Reducers need an intial state for when the hook is called so i created one. Also I included the main functionalities I wanted to access in the App component which are mostly setter functions.
The context provider was created next which includes the paginationReducer
itself:
export const PaginationProvider=({children})=>{
const paginationReducer=(state, action)=>{
const {type, payload}= action
switch (type){
case 'CHANGE_FIRST_INDEX':
return {
...state,
firstIndex:payload
}
case 'CHANGE_LAST_INDEX':
return {
...state,
lastIndex:payload
}
case 'change_page':
return{
...state,
currentPage:payload
}
case 'change_page_count':
return{
...state,
pageCount:payload
}
case 'change_repos':
return{
...state,
repos:payload
}
default:
return state
}
}
The useReducer
is then called and all the setter functions were created in the same format as the toSetFirstIndex
below:
const [state, dispatch]= useReducer(paginationReducer, INITIAL_STATE)
const toSetFirstIndex=(idx)=>{
dispatch({type:'CHANGE_FIRST_INDEX', payload:idx})
}
Part 3: New Features
I added a Search
page which basically included a search bar with an onChange
handler. The handler's job is to fetch any profiles matching the value of the searchbox:
const handleChange=async (e)=>{
if( e.target.value){
await fetch(`https://api.github.com/users/${e.target.value}`)
.then(res=>res.json())
.then(res=>{
if(res.status==404){
setShowError(true)
} else
{
setShowError(false)
setName(res.name)
setImage(res.avatar_url)
}})
await fetch(`https://api.github.com/users/${e.target.value}/repos?per_page=${reposPerPage}&page=${currentPage}`)
.then(res=>res.json())
.then(res=>{
setRepos(res)})
}
}
In the end if a repository is found to match what is in the searchbox, a div is displayed showing the name and profile picture of the user, also some of the user's repositories are shown if the profile picture is clicked on:
return(
<div >
<h1>search page</h1>
<input type='search' onChange={handleChange} />
{showError &&<h2>userNotFound</h2>}
<div className='search-result'>
<img onClick={handleClick} className="search-img" src={image}/>
<p>{name}</p>
{showRepos&&<Cards repos={repos}/> }
</div>
</div>
)
Conclusion
This was quite a challenging project at times despite how simple it looked on paper but I am glad i got it done. The final outcome
can be found here and the repository of the project can be found here,
Thanks a lot for reading!
Top comments (0)