Easily restore your project to a previous version with our new Instant One-click Backup Recovery

How to implement pagination in React

We'll look at how to implement pagination in React by retrieving content from Hygraph and dividing it across multiple pages.
Aagam Vadecha

Last updated by Aagam

Aug 14, 2024

Originally written by Joel

Mobile image

React is a JavaScript frontend library for creating user interfaces that are free and open source. A user interface can be anything that runs on the internet, such as a website or a web application. This website or web application may contain many contents, necessitating pagination to assist users in locating content and keeping our pages from becoming overburdened with content.

In this article, we will explain why is React pagination important and how to implement it.

#What is pagination and why is it important?

Pagination is an organization method to show website content to the users evenly. Just like a book has proper numbering at the bottom of each page, a website or a web application with a lot of data implements pagination to distribute content evenly across pages. This is necessary for a good user experience as a user will likely get confused if he/she sees a hundred records at once.

Also, pagination helps the frontend to fetch the data piece by piece instead of getting everything at once which in turn reduces the loading time of the web pages.

#How to implement pagination in React

To implement pagination in React, first, the server API should be pagination compatible which means it should support pagination parameters that can be used to effectively query the data piece by piece.

Second, we will need frontend components that can query the data piece by piece. In this article, we will use Hygraph API to fetch some paginated content and build a React frontend which will get data from the server and display it on the screen. If you do not have a server-side setup and want to follow along please go through this guide and get your API ready within minutes.

Backend API

For all the models in our schema, Hygraph supports the following parameters to query the data.

ArgumentTypeDefinition
firstIntSeek forwards from the start of the result set.
lastIntSeek backward from the end of the result set.
skipIntSkip the result set by a given value.
beforeStringSeek backward before a specific ID.
afterStringSeeks forward after a specific ID.

We have a simple Post schema with two main fields title and body

Post_Schema.PNG

Let us understand the Hygraph API first, to get the total count of Posts and the actual first five posts we can fire this query in the API playground.

# Query
query getPostList($first: Int, $skip: Int) {
postsConnection {
aggregate {
count
}
}
posts(first: $first, skip: $skip) {
title
}
}
# Variables
{
"first": 5,
"skip": 0
}

BlogPostSchema.PNG

We can manipulate the skip and first variables and get the Post data piece by piece.

React App

Prerequisites

You can create a React Application using Vite, choosing the TypeScript template is preferable. We are using Material UI as the UI framework and Apollo client as the graphql client for this example, but please feel free to use any UI framework / GraphQL client of your choice.

Pagination Example

First, let us add the query to our queries file in the frontend

graphql/queries.ts

import { gql } from "@apollo/client";
export const getPostList = gql`
query getPostList($first: Int, $skip: Int) {
postsConnection(first: $first, skip: $skip) {
aggregate {
count
}
}
posts(first: $first, skip: $skip) {
id
title
body
}
}`

Now let us build a very simple custom pagination component, it should have a single responsibility - To show buttons for different page numbers and whenever one of the buttons is clicked it should emit an event to the parent component for the same.

navigation/pagination.tsx

import { Button, ButtonGroup } from "@mui/material";
interface ICustomPaginationProps {
totalPages: number;
page: number;
onChange: (pageNumber: number) => void;
}
export function CustomPagination({
totalPages,
page,
onChange,
}: ICustomPaginationProps) {
const pageNumbers = Array.from({ length: totalPages }, (_, i) => i + 1);
return (
<ButtonGroup variant="outlined" aria-label="Basic button group">
{pageNumbers.map((number) => {
const buttonColor = number === page ? "primary" : "inherit";
return (
<Button
key={number}
onClick={() => onChange(number)}
color={buttonColor}
>
{number}
</Button>
);
})}
</ButtonGroup>
);
}

This pagination component accepts the props totalPages, page, and an onChange function. It generates buttons in the UI for all page numbers and highlights the current page number, whenever one of the buttons is clicked it will emit an event to the parent component with the page number data.

Now, let us build the parent component to display the list of blogs step-by-step:

import { useState } from "react";
import { Container } from "@mui/material";
...
const DEFAULT_PAGE = 1;
const DEFAULT_PAGE_SIZE = 5;
export function PostList() {
const [currentPage, setCurrentPage] = useState(DEFAULT_PAGE);
const handlePageChange = (value: number) => {
setCurrentPage(value);
};
...
return (
<Container>
...
</Container>
)
}

The default page number is 1 and the page size is 5, we have set up a state variable currentPage to keep track of the current page and a function handlePageChange to manipulate the state variable. We will pass this handlePageChange as a prop to our CustomPagination component.

import { useQuery } from "@apollo/client";
import { useEffect } from "react";
import { getPostList } from "../../graphql/queries";
import { CircularProgress } from "@mui/material";
...
export function PostList() {
...
const { loading, error, data, refetch } = useQuery(getPostList, {
variables: {
first: DEFAULT_PAGE_SIZE,
skip: DEFAULT_PAGE_SIZE * (currentPage - 1),
},
});
useEffect(() => {
refetch({
first: DEFAULT_PAGE_SIZE,
skip: (currentPage - 1) * DEFAULT_PAGE_SIZE,
});
}, [currentPage, refetch]);
if (loading) return <CircularProgress />;
if (error) return <p>Error : {error.message}</p>;
const totalPosts = data.postsConnection.aggregate.count;
const totalPages = Math.ceil(totalPosts / DEFAULT_PAGE_SIZE);
...
}

Next, we have used useQuery from Apollo to get data from the backend and refetch via a useEffect hook whenever the currentPage changes. From the server response, we have calculated the total number of pages. This we will pass as a prop to the CustomPagination component we built earlier.

That’s it, the final component with markup will look something like this:

features/posts-list.tsx

import { useQuery } from "@apollo/client";
import { useEffect, useState } from "react";
import { getPostList } from "../../graphql/queries";
import { CustomPagination } from "../navigation/pagination";
import {
Grid,
Container,
CircularProgress,
Typography,
Box,
} from "@mui/material";
const DEFAULT_PAGE = 1;
const DEFAULT_PAGE_SIZE = 5;
export function PostList() {
const [currentPage, setCurrentPage] = useState(DEFAULT_PAGE);
const { loading, error, data, refetch } = useQuery(getPostList, {
variables: {
first: DEFAULT_PAGE_SIZE,
skip: DEFAULT_PAGE_SIZE * (currentPage - 1),
},
});
useEffect(() => {
refetch({
first: DEFAULT_PAGE_SIZE,
skip: (currentPage - 1) * DEFAULT_PAGE_SIZE,
});
}, [currentPage, refetch]);
if (loading) return <CircularProgress />;
if (error) return <p>Error : {error.message}</p>;
const totalPosts = data.postsConnection.aggregate.count;
const totalPages = Math.ceil(totalPosts / DEFAULT_PAGE_SIZE);
const handlePageChange = (value: number) => {
setCurrentPage(value);
};
return (
<Container>
<Grid container direction="column" style={{ minHeight: "80vh" }}>
<Grid item xs>
{data.posts.map(({ id, title, body }) => (
<Box key={id} my={4}>
<Typography variant="h5">{title}</Typography>
<Typography>{body}</Typography>
</Box>
))}
</Grid>
<Grid item container justifyContent="center">
<CustomPagination
page={currentPage}
totalPages={totalPages}
onChange={handlePageChange}
/>
</Grid>
</Grid>
</Container>
);
}

CustomPaginationComponentDemo-ezgif.com-video-to-gif-converter.gif

Extracting Pagination Logic

We built the PostList component with the entire working pagination functionality of end-to-end, but what if we want to build a new component that is supposed to display a UserList, and then another component that is supposed to display a ProductList. Currently, our Pagination logic is coupled with the PostList component and we will end up duplicating the same logic in future UserList or ProductList components.

Instead, we can extract the pagination related logic from our PostList in a custom hook usePaginatedQuery and then try to reuse this hook in our components. This way we would not need to manage pagination-related state everywhere.

hooks/usePaginatedQuery

import { useState, useEffect } from "react";
import { DocumentNode, useQuery } from "@apollo/client";
const DEFAULT_PAGE = 1;
const DEFAULT_PAGE_SIZE = 5;
export function usePaginatedQuery(
query: DocumentNode,
totalCountPath: string,
variables: Record<string, unknown>
) {
const [currentPage, setCurrentPage] = useState(DEFAULT_PAGE);
const { loading, error, data, refetch } = useQuery(query, {
variables: {
...variables,
first: DEFAULT_PAGE_SIZE,
skip: DEFAULT_PAGE_SIZE * (currentPage - 1),
},
});
useEffect(() => {
refetch({
...variables,
first: DEFAULT_PAGE_SIZE,
skip: (currentPage - 1) * DEFAULT_PAGE_SIZE,
});
}, [currentPage, refetch, variables]);
const totalCount = data?.[totalCountPath].aggregate.count || 0;
const totalPages = Math.ceil(totalCount / DEFAULT_PAGE_SIZE);
const handlePageChange = (page: number) => {
setCurrentPage(page);
};
return { loading, error, data, totalPages, handlePageChange };
}

Now we can use this hook in our component as shown below

features/display-posts.tsx

import { getPostList } from "../../graphql/queries";
import { usePaginatedQuery } from "../../hooks/usePaginatedQuery";
import { CustomPagination } from "./pagination";
import {
Grid,
Container,
CircularProgress,
Typography,
Box,
} from "@mui/material";
export function DisplayPosts() {
const { loading, error, data, totalPages, handlePageChange } =
usePaginatedQuery(getPostList, 'postsConnection', {});
if (loading) return <CircularProgress />;
if (error) return <p>Error : {error.message}</p>;
return (
<Container>
<Grid container direction="column" style={{ minHeight: "80vh" }}>
<Grid item xs>
{data.posts.map(({ id, title, body }) => (
<Box key={id} my={4}>
<Typography variant="h5">{title}</Typography>
<Typography>{body}</Typography>
</Box>
))}
</Grid>
<Grid item container justifyContent="center">
<CustomPagination
totalPages={totalPages}
onChange={handlePageChange}
/>
</Grid>
</Grid>
</Container>
);
}

Material UI Pagination Component

We have built a custom pagination React component to support our needs, this component can be expanded further with prev and next page buttons, and also to support advanced use cases where there are hundreds of pages, we should show UI buttons for only the first few and last few pages. However, if your project is already using a UI framework like Material or AntD, they provide all these functionalities out of the box.

To use the Pagination component from Material UI we need to give it similar props as our CustomPagination component.

features/display-posts.tsx

import { Pagination } from "@mui/material";
// ...existing code
export function DisplayPosts() {
const {
loading,
error,
data,
currentPage,
totalPages,
handlePageChange,
handlePageChangeMui,
} = usePaginatedQuery(getPostList, "postsConnection", {});
// ... all existing code
return (
// ...all existing code
<Pagination
page={currentPage}
count={totalPages}
onChange={handlePageChangeMui}
/>
);
}

The onChange event from Material UI’s Pagination emits two parameters - event and new page number. Let us write a handler function for the same inside our usePaginationQuery hook.

hooks/usePaginatedQuery

// Add this function before the return statement
const handlePageChangeMui = (_, page: number) => {
setCurrentPage(page);
};
return {
// ... all existing code,
handlePageChangeMui,
};

That’s it, this is how it looks now:

MUIPaginationComponentDemo-ezgif.com-video-to-gif-converter.gif

Do notice it provides the prev and next buttons right away, also the component enables/disables those buttons and their css based on the current page value. You can find the entire API for this component with all available props and different customization options here.

#Conclusion

In conclusion, we went through what pagination is, and its importance. We also understood that it is implemented on almost every website or web application that we use.

This article demonstrated how to implement pagination using the Hygraph API and a React frontend. We built a custom pagination component and a reusable pagination hook, ensuring that the pagination logic is decoupled and easily reusable across different components. Additionally, we explored using Material UI’s built-in pagination component for more advanced features and customization.

Blog Authors

Share with others

Sign up for our newsletter!

Be the first to know about releases and industry news and insights.