Photo by Praveen Thirumurugan on Unsplash
Pagination of github respositories fetched from github api using react-query
A beginner tutorial on react-query,pagination and handling UI errors through react-error-boundary.
PermalinkIntroduction
In this article, I'll be covering fetching data using react-query, paginating the list of data and using react-error-boundary to handle ui errors.
I used tailwind for this project and I won't be focusing on styling. Here is how to get tailwind installed.
Here is the project's source code and here is the demo.
PermalinkWhy React-Query
Using react-query, you can implement, along with data fetching, caching and synchronization of your data with the server. It removes the need for writing useEffect to make requests on mount and it includes useful state variables like isLoading , error , data and many others.
PermalinkPrerequisites
Before starting with this tutorial, ensure you have Nodejs downloaded. You can check if you already have Nodejs by typing node -v
in your vscode terminal or command prompt to check for the latest version.
PermalinkInstallations
We would be using vite to get started with our app, we can directly specify the project name and the template we want to use via additional command line options to help scaffold react into our app.
npm create vite@latest github-repo -- --template react
After the installation we would cd into the folder then run npm install
cd github-repo
npm install
Also we'd install packages that are needed for this application.
npm i react-query react-error-boundary react-router-dom
PermalinkGetting Started
To use the react query client in our app, it must be placed at a high level in our app's component hierarchy. The best place to put it is in main.jsx which wraps up our entire app's component. Then, we initialize a new Query Client.
In our main.jsx here is the full code.
Import React from "react";
import { QueryClient, QueryClientProvider } from "react-query";
import {BrowserRouter } from "react-router-dom"
import "./index.css"
import GithubRepos from "./GithubRepos";
const queryClient = new QueryClient();
ReactDOM.createRoot(document.getElementById("root")).render(
<React.StrictMode>
<QueryClientProvider client=queryClient>
<BrowserRouter>
<GithubRepos />
</BrowserRouter>
</QueryClientProvider>
</React.StrictMode>
)
You'd notice I have a GithubRepos
component in the main.jsx. I didn't want the App.jsx to be bloated which is why I created a new component where I place App.jsx and wrapped this component with ErrorBoundary.
import { ErrorBoundary } from "react-error-boundary";
import { useNavigate } from "react-router-dom";
import App from "./App";
import Fallback from "./components/Fallback";
function GithubRepos() {
const navigate = useNavigate();
return (
<>
<ErrorBoundary
FallbackComponent={Fallback}
onReset={() => {
navigate("/");
}}
>
<App />
</ErrorBoundary>
</>
);
}
export default GithubRepos;
PermalinkErrorBoundary
ErrorBoundary helps in handling react ui errors, that is, it catches errors that occur in render method and hooks like useEffect. It does not handle errors in
Event handlers
Asynchronous code (e.g. setTimeout or requestAnimationFrame callbacks)
Server-side rendering
Errors thrown in the error boundary itself (rather than its children)
The ErrorBoundary has a FallbackComponent
prop that takes in a component that renders instead of having a white screen staring at us. It also has an onReset
prop that helps in resetting the state of our app so the error doesn't happen again. In our case here, we will redirect to the homepage.
Here is what our Fallback Component will look like.
import { Link } from "react-router-dom";
function Fallback({ error, resetErrorBoundary }) {
return (
<div role='alert' className='fallback'>
<div className='container font-playfair flex flex-col items-center justify-center h-screen gap-y-2'>
<p className='text-zinc-700 text-xl'>Oops! Something is wrong!</p>
<p style={{ color: "red", maxWidth: "30rem", lineHeight: "28px" }}>
{error.message}
</p>
<button
onClick={resetErrorBoundary}
className='bg-gitGreen text-neutral-50 py-2 px-3'
>
Go Back Home
</button>
</div>
</div>
);
}
export default Fallback;
The Fallback component receives two props in the above code. The error
prop and the resetErrorBoundary
prop. The error
prop is an object therefore we use the message property in it. While the resetErrorBoundary
prop is a callback function that triggers the function in our onReset
prop in the ErrorBoundary that wraps our GithubRepos
component.
PermalinkCustom Hook
I created a hooks folder where there is a file named useFetch2 (forgive me for the naming). Inside this file, we will create a fetch data function using async and await. This function will return our data and it will be passed to our useQuery
function from react-query.
import { useQuery } from "react-query";
const fetchData = async (url) => {
try {
const resp = await fetch(url);
const data = await resp.json();
return data;
} catch (err) {
throw new Error("DB connection failed", { cause: err });
}
};
export const useFetchedUrl = (url, urlType) => {
return useQuery(urlType, async () => await fetchData(url), {
useErrorBoundary: (error) => error.response?.status >= 500,
});
};
The exported useFetchedUrl
function takes in two arguments, the url
and urlType
The url
will be our endpoint and the urlType
will serve as a key that differentiates our queries from one another.
This function then returns a useQuery
from our react-query
which takes a queryKey in our case urlType
. It also takes in a callback function in this case is our fetchData
and an optional object that has a useErrorBoundary property (this is optional).
PermalinkFetching Data
In the source code , you'd find a pages folder in the src folder. Inside of it is a Repos component where our fetching will take place.
The custom hook we have created will be imported and used here. It will help in fetching data from different APIs without having to repeat code. Here I will be fetching from two different endpoints.
endpoint that displays my bio and follower counts
endpoint that shows a list of all my repos.
Because I'm fetching from an api with two different endpoints, using a query key (urlType) is important as it prevents refetching of the api if the key is unchanged.
I am also using aliases to make our state variables unique. This helps with readability.
In the code below, we will have a SyntaxError as there is a redeclaration of our data and isLoading variables.
const { data, isLoading } = useFetchedUrl(
"https://api.github.com/users/{YOUR_GITHUB_USERNAME}",
"headerData"
);
const { data, isLoading } = useFetchedUrl(
"https://api.github.com/users/{YOUR_GITHUB_USERNAME}/repos",
"reposData"
);
To avoid the error
const { data:headerData, isLoading:headerLoader } = useFetchedUrl(
"https://api.github.com/users/{YOUR_GITHUB_USERNAME}",
"headerData"
);
const { data:reposData, isLoading:reposLoader } = useFetchedUrl(
"https://api.github.com/users/{YOUR_GITHUB_USERNAME}/repos",
"reposData"
);
Now I have different variables for the different data fetched. Also you will notice the useFetchedUrl is taking in two variables, which are the url and the urlType which serves as our unique key for the data fetched.
The data is then passed down to children components that needs it. The headerData is going to be an object that shows information about you while the reposData will is an array of objects with different repositories.
Repos component will look like this
import RepoHeader from "../components/Repos/RepoHeader";
import RepoSticky from "../components/Repos/RepoSticky";
import RepoMain from "../components/Repos/RepoMain";
import { Outlet } from "react-router-dom";
import { useFetchedUrl } from "../hooks/useFetch2";
import Loader from "../utility/Loader";
const Repos = () => {
const { data: headerData, isLoading: headerLoader } = useFetchedUrl(
"https://api.github.com/users/talentlessDeveloper",
"headerData"
);
const { data: reposData, isLoading: reposLoader } = useFetchedUrl(
"https://api.github.com/users/talentlessDeveloper/repos",
"reposData"
);
if (headerLoader || reposLoader) return <Loader />;
console.log({ headerData, reposData });
return (
<section >
<div>
<RepoHeader data={headerData} />
<RepoSticky />
<RepoMain data={reposData} />
</div>
<Outlet />
</section>
);
};
export default Repos;
In the Repos component, we are importing useFetchedUrl
from our hooks folder. Remember our custom hook the useFetchedUrl
returns a useQuery function from react-query. The beauty of this is we would have so many state variables afforded to us. Apart from the returned data, we would also have isLoading,isSuccess, error, isIdle and many others depending on the state we need. The states , including the data are all returned in an object, reason we had to destructure our useFetchedUrl function in the above code.
The RepoHeader component receiving the headerData
const RepoHeader = ({ data }) => {
return (
<>
{" "}
<div >
<div>
<img
src={data.avatar_url}
alt='talentless Developer profile'
layoutId='entranceImg'
/>
<div>
<h1 >
<span>{data.name}</span>
<span>{data.login}</span>
</h1>
</div>
</div>
<div >
<h2 >{data.bio}</h2>
<div>
<a href='mailto:kareemope52@gmail.com'>kareemope52@gmail</a>
</div>
<div >
<p>
{data.followers} followers. {data.following} following
</p>
</div>
</div>
</div>
</>
);
};
export default RepoHeader;
Now I have my data and loading states. I also created another useFetch hook without using react-query, you can compare both and see how many lines react-query saves us and react-query even offers more robustness!
PermalinkPagination
In our RepoMain Component there we would be paginating our repositories.
Firstly, we would have a page
state to monitor what page we are in. We would also be having a reposPerPage
variable to hold the number of repositories we want appearing per page.
Since we would be using the javascript slice method for our pagination, there will be a start and end variable to indicate the indexes.
Also a pageLength
to ensure we have a dynamic length of buttons depending on our array length and number of reposPerPage
.
const [page, setPage] = useState(1);
const reposPerPage = 8;
let repos = data;
const start = (page - 1) * reposPerPage;
const end = page * reposPerPage;
const pageLength = Math.ceil(repos.length / reposPerPage);
In addition, we would have prev and next buttons to handle our page state onClick.We would also have a disabled attribute to ensure we don't click outside of the available pages range.
<button
disabled={page <= 1}
onClick={() => setPage((p) => p - 1)}
>
<span>Previous</span>
</button>
<button
disabled={page >= pageLength}
onClick={() => setPage((p) => p + 1)}
>
<span>Next</span>
</button>
When we click the next button, we increase our page
state by one and we decrease our page state by one by clicking the previous button. The previous button is disabled when the page state is one and the next button is disabled when the page
state is greater than or equal to the pageLength
variable.
For the number buttons, the javascript Array.from method is helpful in creating copies of array.
Array.from({length:10}, (_, i) => i + 1)
// [1,2,3,4,5,6,7,8,9,10]
The method above will return an array of ten (10) numbers. Hence we can use this to create an array of buttons numbered one till our pageLength
. Then each button onClick sets our page state to whatever number they have.
Array.from({ length: pageLength }, (_, i) => i + 1).map(
(btn) => (
<button
key={`${btn}-i`}
onClick={() => setPage(btn)}
>
{btn}
</button>
)
)
Here is the full code
const [page, setPage] = useState(1);
const reposPerPage = 8;
let repos = data;
const start = (page - 1) * reposPerPage;
const end = page * reposPerPage;
const pageLength = Math.ceil(repos.length / reposPerPage);
return <>
<ul>
{repos.slice(start, end).map((repo) => {
return <RepoCard key={repo.id} repo={repo} />;
})}
</ul>
<div className={`flex items-center gap-x-2`}>
<button
disabled={page <= 1}
onClick={() => setPage((p) => p - 1)}
>
<span>Previous</span>
</button>
<div >
{Array.from({ length: pageLength }, (_, i) => i + 1).map(
(btn) => (
<button
key={`${btn}-i`}
onClick={() => setPage(btn)}
>
{btn}
</button>
)
)}
</div>
<button
disabled={page >= pageLength}
onClick={() => setPage((p) => p + 1)}
>
<span>Next</span>
</button>
</div>
</>
PermalinkConclusion
We have been able to fetch data from the github api using react-query by creating a useFetchedUrl hook that returns useQuery function which in turn returns an object of stateful variables.
We also learned to paginate our list of repositories using the javascript slice method and using the Array.from method to create dynamic numbers of buttons depending on the page length.
This is my first technical writing so a lot of things aren't there yet. Feel free to leave a comment or contact me regarding questions, suggestions, any errors in the post or anything you want to discuss.
Have fun coding!