How to Implement Pagination in React
In this article, we'll look at how to implement pagination in React by retrieving content from Hygraph and dividing it across multiple pages.
How to Implement Pagination in ReactAnchor
React is a JavaScript front-end 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.
What Is Pagination, and Why Is It Important?Anchor
Pagination is the process of dividing a website's content into a series of pages with similar content. This improves the user experience because the initial page load is faster than loading all the content simultaneously.
Pagination is an excellent method for organizing website content into separate pages so users can find the desired page/content. It is a feature we can use on a blog page, a product page, or any other page with a lot of content that we want to distribute across multiple pages.
Pagination in ReactAnchor
Implementing pagination in React can be time-consuming because it necessitates some logic. This guide will look at two approaches to implementing this logic in React: from scratch and using a React package.
Before we begin implementing pagination, let's build a mini-project with Hygraph in which we consume a large amount of data and divide it into several pages with a set number of data per page.
Building a Blog PageAnchor
We'd create a page with a list of blog posts and a link to where we can read them. For example, if an author has content published across many platforms, this could be the best way to join all his/her content into a single blog page, making it easier for people to access.
Because this article is about pagination, we'll go over the app creation briefly, and if you want to look at the source code, you can find it at this GitHub repository. Also, here is a live link to check it out.
Building Markup Page - App.jsAnchor
We would handle all logic and display all of our content in our App.js without needing other components; however, you may choose to split this into components depending on your needs. This is how the markup looks:
// App.jsimport React, { useState, useEffect } from 'react';const posts = [{id: 1,title:'How to Internationalize a React Application Using i18next and Hygraph',excert:'In this post, we will take a deep dive into how to internationalize a React Application using i18next and Hygraph',postUrl: 'https://hygraph.com/blog/react-internationalization',cover: {url: 'https://images.unsplash.com/photo-1518791841217-8f162f1e1131?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=800&q=60',},datePublished: '2020-01-01',author: {profilePicture: {url: 'https://images.unsplash.com/photo-1518791841217-8f162f1e1131?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=800&q=60',},},},];const App = () => {const [blogPosts, setBlogPosts] = useState(posts);return (<div className="container"><div className="title"><h1>Blog</h1></div>{blogPosts ? (<div className="blog-content-section"><div className="blog-container">{blogPosts.map((blogPost) => (<div className="blog-post" key={blogPost.id}><img className="cover-img" src={blogPost.cover.url} alt="" /><h2 className="title">{blogPost.title}</h2><p className="description">{blogPost.excert}</p><div className="card-details"><div className="lh-details"><imgclassName="author-img"src={blogPost.author.profilePicture.url}alt=""/><p className="date">{new Date(`${blogPost.datePublished}`).toLocaleDateString('en-us',{year: 'numeric',month: 'short',day: 'numeric',})}</p></div><ahref={blogPost.postUrl}target="_blank"rel="noopener noreferrer"className="read-more">Read post</a></div></div>))}</div></div>) : (<div className="loading">Loading...</div>)}</div>);};export default App;
Note: We are using static data to populate our page with content, which is why we created the posts array, but such content would be best retrieved from an API built with Hygraph. This repository also contains the styles in the index.css file.
Here is my schema: I created two models, one for posts and one for authors, which are linked to each other in a two-way reference.
Posts SchemaAnchor
Author SchemaAnchor
Fetching Content from HygraphAnchor
In react, querying is done with **graphql-request**
and for us to do that in React, we first need to install the dependency by running the command below:
npm i graphql-request
After it has been successfully installed, we will import it into our App.js file so that we can query the Hygraph endpoint:
import { request } from 'graphql-request';
Querying with GraphQL in ReactAnchor
At this point, it is assumed that we have our Hygraph endpoint and our GraphQL query; my query looks like this:
{posts {idtitleexcertpostUrlcover {url}datePublishedauthor {firstNameprofilePicture {url}}}}
Note: In the above query, we are requesting that all blog posts be saved with all the information we will be using, such as the cover image, title, author image, and much more.
At this point, this is what our React app - App.js
would look like:
import React, { useState, useEffect } from 'react';import { request } from 'graphql-request';const App = () => {const [blogPosts, setBlogPosts] = useState([]);useEffect(() => {const fetchBlogPosts = async () => {const { posts } = await request('https://api-us-east-1.hygraph.com/v2/cl3zo5a7h1jq701xv8mfyffi4/master',`{posts {idtitleexcertpostUrlcover {url}datePublishedauthor {firstNameprofilePicture {url}}}}`);setBlogPosts(posts);};fetchBlogPosts();}, []);return (<div className="container"><div className="title"><h1>Blog</h1></div>{blogPosts ? (<div className="blog-content-section"><div className="blog-container">{blogPosts.map((blogPost) => (<div className="blog-post" key={blogPost.id}><img className="cover-img" src={blogPost.cover.url} alt="" /><h2 className="title">{blogPost.title}</h2><p className="description">{blogPost.excert}</p><div className="card-details"><div className="lh-details"><imgclassName="author-img"src={blogPost.author.profilePicture.url}alt=""/><p className="date">{new Date(`${blogPost.datePublished}`).toLocaleDateString('en-us',{year: 'numeric',month: 'short',day: 'numeric',})}</p></div><ahref={blogPost.postUrl}target="_blank"rel="noopener noreferrer"className="read-more">Read post</a></div></div>))}</div></div>) : (<div className="loading">Loading...</div>)}</div>);};export default App;
Note: Replace the API Endpoint and possibly the query with your own, and tinker with the UI if desired.
At this point, we have all the contents we populated our Hygraph database with on our app, which would be too much to fetch at once into our website if we had up to 100. Let's now use Pagination to help us divide this into multiple pages.
Implementing Pagination in ReactAnchor
To implement pagination, we would need to create and obtain some data that we would use to implement this pagination, such as the number of posts per page and the index of both the first and last posts of a specific page, so that we can use the slice()
method to ensure that only the following content is displayed:
import React, { useState, useEffect } from 'react';import { request } from 'graphql-request';const App = () => {const [blogPosts, setBlogPosts] = useState([]);const [currentPage, setCurrentPage] = useState(1);const [postsPerPage] = useState(3);// ...const indexOfLastPost = currentPage * postsPerPage;const indexOfFirstPost = indexOfLastPost - postsPerPage;const currentPosts = blogPosts.slice(indexOfFirstPost, indexOfLastPost);return (<div className="container">{/* ... */}</div>);};export default App;
Note: To make this code easier to understand, we removed the useEffect()
and markup. I'll keep doing something like this throughout this article, so check this repository whenever you're confused.
In the preceding code, we created two states: one to hold the current page and another to hold the number of posts we want to display on a page. To get the total number of pages, we simply divide the total number of content by the number of posts per page, which we will do shortly.
We also created variables to store the index of a page's first and last post. For example, if we want to show three posts per page, the indexOfLastPost
of page 2 will be 6
and the indexOfFirstPost
will be 3
.
Now that we've used the splice()
method to help us get the content we want to display on each page, make sure we change our array from blogPosts
to currentPosts
or any variable we use to store the spliced array.
When we change the currentPage
's state value, the following happens to the content on that page:
We now need to add a page number to the bottom of our page so that users can easily navigate through these pages.
This can be accomplished either manually by creating a component to handle all the logic, or by installing a package to handle all the logic for us. In this article, we'll look at both options, but first, let's handle it manually by creating a paginate.js
component.
Pagination componentAnchor
We would receive two props for now from the App.js component (parent component) in the pagination component: postsPerPage
and totalPosts
. These data will assist us in determining the total number of pages for our content.
After receiving this data, we will create an empty array to store the page numbers from the loop:
import React from 'react';const Paginate = ({ postsPerPage, totalPosts }) => {const pageNumbers = [];for (let i = 1; i <= Math.ceil(totalPosts / postsPerPage); i++) {pageNumbers.push(i);}return (<div className="pagination-container">{/* ... */}</div>);};export default Paginate;
Essentially, the code above loops through the based on the number of pages to get the numbers as 1,2,3... and then pushes it to an array that we can now loop through in our app:
import React from 'react';const Paginate = ({ postsPerPage, totalPosts }) => {const pageNumbers = [];for (let i = 1; i <= Math.ceil(totalPosts / postsPerPage); i++) {pageNumbers.push(i);}return (<div className="pagination-container"><ul className="pagination">{pageNumbers.map((number) => (<li key={number} className="page-number">{number}</li>))}</ul></div>);};export default Paginate;
At this point, our page numbers are visible on our application. The next step would be to add a click event to the page numbers, which would allow us to change the currentPage state. This function would be written in the parent component (App.js) and passed down as a prop:
// Navigate.jsimport React from 'react';const Paginate = ({ postsPerPage, totalPosts, paginate }) => {const pageNumbers = [];for (let i = 1; i <= Math.ceil(totalPosts / postsPerPage); i++) {pageNumbers.push(i);}return (<div className="pagination-container"><ul className="pagination">{pageNumbers.map((number) => (<likey={number}onClick={() => paginate(number)}className="page-number">{number}</li>))}</ul></div>);};export default Paginate;
We added a click event to the page number's button, as well as a method that takes the page number and passes it to the App.js component:
import React, { useState, useEffect } from 'react';import { request } from 'graphql-request';import Paginate from './Paginate';const App = () => {const [blogPosts, setBlogPosts] = useState([]);const [currentPage, setCurrentPage] = useState(1);const [postsPerPage] = useState(3);// ...const indexOfLastPost = currentPage * postsPerPage;const indexOfFirstPost = indexOfLastPost - postsPerPage;const currentPosts = blogPosts.slice(indexOfFirstPost, indexOfLastPost);const paginate = (pageNumber) => {setCurrentPage(pageNumber);};return (<div className="container"><div className="title"><h1>Blog</h1></div>{blogPosts ? (<div className="blog-content-section">{/* ... */}<PaginatepostsPerPage={postsPerPage}totalPosts={blogPosts.length}paginate={paginate}/></div>) : (<div className="loading">Loading...</div>)}</div>);};export default App;
You will notice we added this method, which helps us set the currentPage
to the number received:
const paginate = (pageNumber) => {setCurrentPage(pageNumber);};
At this point, when we click the buttons, it changes the content based on the page:
So far, we've been able to use React to implement pagination. Although this looks good and allows us to navigate through all of our contents, we should add Next
and Prev
(previous) buttons. We can easily do this way and then add a onClick()
event that will be handled in our parent component, just like we did earlier:
import React from 'react';const Paginate = ({ postsPerPage, totalPosts, paginate, previousPage, nextPage }) => {const pageNumbers = [];for (let i = 1; i <= Math.ceil(totalPosts / postsPerPage); i++) {pageNumbers.push(i);}return (<div className="pagination-container"><ul className="pagination"><li onClick={previousPage} className="page-number">Prev</li>{pageNumbers.map((number) => (<likey={number}onClick={() => paginate(number)}className="page-number">{number}</li>))}<li onClick={nextPage} className="page-number">Next</li></ul></div>);};export default Paginate;
Essentially, we added two new lists before and after the list iteration, as well as two events that we would handle on our App.js
page:
import React, { useState, useEffect } from 'react';import { request } from 'graphql-request';import Paginate from './Paginate';const App = () => {//...const previousPage = () => {if (currentPage !== 1) {setCurrentPage(currentPage - 1);}};const nextPage = () => {if (currentPage !== Math.ceil(blogPosts.length / postsPerPage)) {setCurrentPage(currentPage + 1);}};return (<div className="container"><div className="title"><h1>Blog</h1></div>{blogPosts ? (<div className="blog-content-section">{/* ... */}<PaginatepostsPerPage={postsPerPage}totalPosts={blogPosts.length}paginate={paginate}previousPage={previousPage}nextPage={nextPage}/></div>) : (<div className="loading">Loading...</div>)}</div>);};export default App;
When we load our app at this point, both the next and previous (previous) buttons will now work and will not work when the max and min numbers are reached, as determined by the if statement we added in the methods.
Using react-paginate packageAnchor
The react-paginate package handles all the logic for us, which means we only need to install the package and use the component by sending in the necessary values, and it handles the core logic for us.
The first step would be to install react-paginate
package using the command below:
npm install react-paginate --save
Once that is successful, we can import and use it in this manner, and everything will be fine:
import React, { useState, useEffect } from 'react';import { request } from 'graphql-request';import ReactPaginate from 'react-paginate';const App = () => {const [blogPosts, setBlogPosts] = useState([]);const [currentPage, setCurrentPage] = useState(1);const [postsPerPage] = useState(3);// ...const indexOfLastPost = currentPage * postsPerPage;const indexOfFirstPost = indexOfLastPost - postsPerPage;const currentPosts = blogPosts.slice(indexOfFirstPost, indexOfLastPost);const paginate = ({ selected }) => {setCurrentPage(selected + 1);};return (<div className="container"><div className="title"><h1>Blog</h1></div>{blogPosts ? (<div className="blog-content-section">{/* ... */}<ReactPaginateonPageChange={paginate}pageCount={Math.ceil(blogPosts.length / postsPerPage)}previousLabel={'Prev'}nextLabel={'Next'}containerClassName={'pagination'}pageLinkClassName={'page-number'}previousLinkClassName={'page-number'}nextLinkClassName={'page-number'}activeLinkClassName={'active'}/></div>) : (<div className="loading">Loading...</div>)}</div>);};export default App;
Note: You'll notice that the ReactPaginate
component has a onPageChange
method that handles the core logic of changing pages by updating the value of the currentPage
state. It is critical to understand that ReactPaginate
contains an object with the value of selected
that contains the page number.
We also set the classes so that we can style the page navigation using the same CSS class names as before. You can check their documentation to understand the package better and other available options available.
So far, we've seen how React implementation can be handled by retrieving all of our contents before using the splice method to show only the ones that match the currentPage
set.
When working with real-world applications and fetching content from an API like Hygraph, it becomes a bad idea to get all data first as we did earlier because the site will slow down trying to get thousands of data while only a few will be shown to the users at once.
Hygraph makes it simple for us to handle content pagination by utilizing the following arguments:
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 result set by given amount. |
before | String | Seek backward before specific ID. |
after | String | Seeks forwards after specific ID. |
posts (first: 5) {idtitle}
Let's now use some of these arguments to add pagination to our application. We will also use the relay specification to determine the pageSize
(this is very important to calculate the pages and display page numbers as we have seen earlier). This is how our schema will now look with the useEffect()
hook:
useEffect(() => {const fetchBlogPosts = async () => {const { posts, postsConnection } = await request('https://api-us-east-1.hygraph.com/v2/cl3zo5a7h1jq701xv8mfyffi4/master',`{posts (first: ${postsPerPage}, skip: ${currentPage * postsPerPage - postsPerPage}) {idtitleexcertpostUrlcover {url}datePublishedauthor {firstNameprofilePicture {url}}}postsConnection {pageInfo {pageSize}}}`);setBlogPosts(posts);setTotalPosts(postsConnection.pageInfo.pageSize);};fetchBlogPosts();}, [currentPage, postsPerPage]);
Looking at the schema, we can see that the first
and skip
arguments have been added to get the exact data for each page. We also use the relay specification to get the pageSize
from our postsConnection
object.
As we can see, we already saved the pageSize
value to the state via this code setTotalPosts(postsConnection.pageInfo.pageSize)
.
All that remains is to ensure that the totalSize
value is passed to the paginate component and that the condition set for the nextPage
button is amended.
We can see the updated code for our App.js
file that implements pagination with Hygraph and React in this GitHub Gist.
Note: In the GitHub repository, we also created 3 branches that hold code for the 3 different methods covered in this article, with links to each branch in the README file.
ConclusionAnchor
We learned how to implement pagination in our React application from scratch by handling all the logic and also using the react-paginate package in this guide. It is up to you to use any of the methods, but it is important to note that installing an extra package increases the size of your project and can also affect the load-time of your project, which may or may not be noticeable.