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.
Argument | Type | Definition |
---|---|---|
first | Int | Seek forwards from the start of the result set. |
last | Int | Seek backward from the end of the result set. |
skip | Int | Skip the result set by a given value. |
before | String | Seek backward before a specific ID. |
after | String | Seeks forward after a specific ID. |
We have a simple Post
schema with two main fields title
and body
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.
# Queryquery getPostList($first: Int, $skip: Int) {postsConnection {aggregate {count}}posts(first: $first, skip: $skip) {title}}# Variables{"first": 5,"skip": 0}
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) {idtitlebody}}`
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 (<Buttonkey={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"><CustomPaginationpage={currentPage}totalPages={totalPages}onChange={handlePageChange}/></Grid></Grid></Container>);}
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"><CustomPaginationtotalPages={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 codeexport function DisplayPosts() {const {loading,error,data,currentPage,totalPages,handlePageChange,handlePageChangeMui,} = usePaginatedQuery(getPostList, "postsConnection", {});// ... all existing codereturn (// ...all existing code<Paginationpage={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 statementconst handlePageChangeMui = (_, page: number) => {setCurrentPage(page);};return {// ... all existing code,handlePageChangeMui,};
That’s it, this is how it looks now:
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.