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.

·

9 min read

Introduction

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.

Why 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.

Prerequisites

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.

Installations

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

Getting 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;

ErrorBoundary

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.

Custom 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).

Fetching 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!

Pagination

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>
</>

Conclusion

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!