Implementation of a paginated infinite scroll using React-Query and Ant Design in Next-js app

Raazesh Prajapati
readytowork, Inc.
Published in
9 min readJun 1, 2023

Are you tired of the traditional pagination approach for fetching new data? Well, get ready for a refreshing change! Introducing the infinite scrolling feature as a captivating alternative to mundane pagination. In this article, we will dive into an exemplary implementation of the infinite scroll for a Next.js app using React Query.

Here is an example of the implementation of Infinite Scroll for the next-js app using React-query.

💻 Implementing Infinite scroll in Next-app:

In this article, we will implement Infinite Scroll with the help of react-query in our next-js (version 13) app.

🌟Getting Started

Before starting our project we should have the following prerequisites in our local system.

📦Prerequisites:

⚙️ Creating and setting up Next-js project:

We are using create-next-app, which sets up everything automatically for us. We want to start with a TypeScript in our project so we should use the --typescript flag. To create a project, run:

# I am using yarn as package manager
yarn create next-app --typescript
# or
npx create-next-app@latest --typescript
# or
pnpm create next-app --typescript

For more about NextJs application, you can visit next-js official documentation here.

During the process, you will be prompted to choose which features to enable for the project. Make sure to select “Yes” for TypeScript and ESLint. Additionally, you will be asked if you would like to use the src/ directory and the experimental app/ directory. I have chosen “Yes” for src/ directory options and “No” for app/ directory options. For the import alias, simply press Tab and Enter to accept.

For good UI I am using ant-design and Styled-component for styling also remove all styling from globals.css. So lets us install and set them up:

yarn add antd styled-components

After all these setups hope our project folder tree looks something like this:

.
├─ .eslintrc.json
├─ .git
├─ .gitignore
├─ .next
├─ README.md
├─ next.config.js
├─ node_modules
├─ package-lock.json
├─ package.json
├─ public
├─ src
│ ├─ pages
│ │ ├─ _app.tsx
│ │ ├─ _document.tsx
│ │ └─ index.tsx
│ └─ styles
│ └─ globals.css
├─ tsconfig.json
└─ yarn.lock

Since we are using a package manager yarn that supports the “resolutions” package.json field, we need to add an entry to it as well corresponding to the major version range. This helps avoid an entire class of problems that arise from multiple versions of styled components being installed in your project.

In package.json:

{
"resolutions": {
"styled-components": "^5"
}
}

Since NextJs13 creates components and pages server side, so we have to configure _document.tsx it to work Styled-component properly.

import Document, { Html, Head, Main, NextScript } from "next/document";
import { ServerStyleSheet } from "styled-components";
export default class MyDocument extends Document {
static async getInitialProps(ctx: any) {
const sheet = new ServerStyleSheet();
const originalRenderPage = ctx.renderPage;
try {
ctx.renderPage = () =>
originalRenderPage({
enhanceApp: (App: any) => (props: any) =>
sheet.collectStyles(<App {...props} />),
});
const initialProps = await Document.getInitialProps(ctx);
return {
...initialProps,
styles: (
<>
{initialProps.styles}
{sheet.getStyleElement()}
</>
),
};
} finally {
sheet.seal();
}
}
render() {
return (
<Html>
<Head />
<body>
<Main />
<NextScript />
</body>
</Html>
);
}
}

🎉 Bravo, our basic next-app setup has been completed.

🍀 Setup React Query in Next.js 13 App:

Now let's install react-query and its dependencies using the following code:

yarn add react-query

In the next step, we will configure react-query it within our Next.js application. To achieve this, we need to import the QueryClientProvider and QueryClient into our app. Then, we’ll wrap our app component in the _app file using the QueryClientProvider. This ensures that the app has access to the QueryClient and can utilize react-query functionalities seamlessly.

src/pages/_app.tsx :

import "antd/dist/reset.css";
import "@/styles/globals.css";
import type { AppProps } from "next/app";
import { QueryClient, QueryClientProvider } from "react-query";

export default function App({ Component, pageProps }: AppProps) {
const client = new QueryClient();
return (
<QueryClientProvider client={client}>
<Component {...pageProps} />
</QueryClientProvider>
);
}

Tada, our general setup for the app is completed.

⚙️ Using useInfiniteQuery hook for implementing infinite-hook:

To incorporate the desired functionality, simply add the following code snippet to your index page:

src/pages/index.tsx :

import { Card, Divider, Spin } from "antd";
import { useCallback, useEffect, useRef } from "react";
import { useInfiniteQuery } from "react-query";
import styled from "styled-components";

const TableContainer = styled.div`
display: contents;
`;
const CustomInfiniteScroll = styled.div``;

const CenteredText = styled.div`
display: flex;
align-items: center;
justify-content: center;
`;
const Container = styled.div`
align-items: center;
justify-content: center;
display: flex;
flex-direction: column;
`;

const maxItemCount = 100;

const ItemList: React.FC = () => {
const observerElem = useRef(null);

/**
*
* this function retrieves data
* from an API endpoint using pagination
* parameters (offset and limit)
* and assigns a nextPage value based on
* the length of the data received
*
*/
const fetchItems = async (page: number) => {
const pageSize = 3;
const response: any = await fetch(
`https://api.escuelajs.co/api/v1/products?offset=${page}&limit=${pageSize}` //using free api for this example
);
const data = await response.json();
data["nextPage"] = data.length > maxItemCount ? page + 1 : null;
return data;
};

/**
*fetching data using React-Query's useInfiniteQuery hook
*this code snippet sets up the infrastructure for paginated
data fetching using useInfiniteQuery
*
*/
const { data, fetchNextPage, isFetchingNextPage, hasNextPage } =
useInfiniteQuery(
["fetch-notification"],
({ pageParam = 1 }) => fetchItems(pageParam),
{
refetchOnWindowFocus: false,
keepPreviousData: false,
getNextPageParam: (lastpage, allPages) => {
const nextPage = allPages.length + 1;
if (lastpage?.nextPage <= maxItemCount) {
return nextPage;
}
return undefined;
},
}
);

/**
*
* This function is trigger fetchNextPage
* when observerElem isvisible in the viewport
*
*/
const handleObserver = useCallback(
(entries: any) => {
const [target] = entries;
if (target.isIntersecting) {
fetchNextPage();
}
},
[fetchNextPage]
);

/**
*
* This function is used to detect when a specific element
* (in our case div with ref observerElem )
* becomes visible in the viewport
*
*/

useEffect(() => {
const element = observerElem.current;
const option = { threshold: 0 };

const observer = new IntersectionObserver(handleObserver, option);
if (element) {
observer.observe(element);
return () => observer.unobserve(element);
}
}, [fetchNextPage, hasNextPage, handleObserver]);
return (
<TableContainer id={"scrollableDiv"}>
{!data ? (
<CenteredText>
<Spin size={"large"} />
</CenteredText>
) : (
<CustomInfiniteScroll>
{data?.pages?.map((group, index_group) => {
return (
<Container key={index_group}>
{group?.map((data: any, index: number) => {
return (
<div key={`${index}-${data?.index || 0}`}>
<Card
hoverable
style={{ width: "350px" }}
cover={
<img
src={data.images[0]}
width={200}
height={200}
style={{ objectFit: "contain", padding: "20px" }}
/>
}
title={data.title || ""}
>
<Card.Meta
title={`Rs.${data.price}` || ""}
description={data.description || ""}
/>
</Card>
<Divider
style={{
marginTop: "10px",
marginBottom: "10px",
}}
/>
</div>
);
})}
</Container>
);
})}
<div className={"loader"} ref={observerElem}>
{isFetchingNextPage && hasNextPage ? (
<div
style={{
display: "flex",
alignItems: "center",
justifyContent: "center",
}}
>
<Spin />
</div>
) : (
<CenteredText>{"No search left"}</CenteredText>
)}
</div>
</CustomInfiniteScroll>
)}
</TableContainer>
);
};

export default ItemList;

Now, let’s break down a few major important codes mentioned above into smaller sections for better understanding and clarity:

Importing Dependencies:

import { Card, Divider, Spin } from "antd";
import { useCallback, useEffect, useRef } from "react";
import { useInfiniteQuery } from "react-query";
import styled from "styled-components";

The code begins by importing necessary dependencies from various libraries, including “antd” for UI components, “react” for React-related functionalities, “react-query” for data fetching, and “styled-components” for styling.

Styling Components:

const TableContainer = styled.div`
display: contents;
`;
const CustomInfiniteScroll = styled.div``;

const CenteredText = styled.div`
display: flex;
align-items: center;
justify-content: center;
`;
const Container = styled.div`
align-items: center;
justify-content: center;
display: flex;
flex-direction: column;
`;

This code defines several styled components using the “styled-components” library. These components are used to structure and style the elements within the “ItemList” component.

Constant and Variable Declarations:

A constant “maxItemCount” is declared to define the maximum number of items that can be fetched. It is set to 100 in this example. The code also declares a variable “observerElem” using the “useRef” hook to store a reference to an element.

Fetching Data:

const fetchItems = async (page: number) => {
const pageSize = 3;
const response: any = await fetch(
`https://api.escuelajs.co/api/v1/products?offset=${page}&limit=${pageSize}` //using free api for this example
);
const data = await response.json();
data["nextPage"] = data.length > maxItemCount ? page + 1 : null;
return data;
};

The “fetchItems” function is defined as an asynchronous function that retrieves data from an API endpoint. It uses pagination parameters (offset and limit) to fetch a specific data set. The function also assigns a “nextPage” value based on the length of the received data. This value is used later to determine if there is more data to fetch.

I have utilized the Platzi Fake Store API to generate mock data for this purpose.

Data Fetching with React-Query:

const { data, fetchNextPage, isFetchingNextPage, hasNextPage } =
useInfiniteQuery(
["fetch-notification"],
({ pageParam = 1 }) => fetchItems(pageParam),
{
refetchOnWindowFocus: false,
keepPreviousData: false,
getNextPageParam: (lastpage, allPages) => {
const nextPage = allPages.length + 1;
if (lastpage?.nextPage <= maxItemCount) {
return nextPage;
}
return undefined;
},
}
);

The above code utilizes the “useInfiniteQuery” hook from React-Query to fetch data. It sets up the infrastructure for paginated data fetching. The hook takes three main parameters: the query key, a function to fetch data based on the page parameter, and configuration options. It returns data, a function to fetch the next page of data, a boolean indicating if the next page is being fetched, and a boolean indicating if there is a next page.

Intersection Observer for Infinite Scrolling:

const handleObserver = useCallback(
(entries: any) => {
const [target] = entries;
if (target.isIntersecting) {
fetchNextPage();
}
},
[fetchNextPage]
);

The code defines the “handleObserver” function using the “useCallback” hook. This function is triggered when the “observerElem” becomes visible in the viewport. It calls the “fetchNextPage” function to fetch the next page of data.

Setting up the Intersection Observer:

useEffect(() => {
const element = observerElem.current;
const option = { threshold: 0 };

const observer = new IntersectionObserver(handleObserver, option);
if (element) {
observer.observe(element);
return () => observer.unobserve(element);
}
}, [fetchNextPage, hasNextPage, handleObserver]);

The “useEffect” hook is used to detect when the “observerElem” becomes visible in the viewport. It creates an instance of the Intersection Observer and observes the element. If the element exists, the hook returns a cleanup function to unobserve the element.

Rendering the Component:

return (
<TableContainer id={"scrollableDiv"}>
{!data ? (
<CenteredText>
<Spin size={"large"} />
</CenteredText>
) : (
<CustomInfiniteScroll>
{data?.pages?.map((group, index_group) => {
return (
<Container key={index_group}>
{group?.map((data: any, index: number) => {
return (
<div key={`${index}-${data?.index || 0}`}>
<Card
hoverable
style={{ width: "350px" }}
cover={
<img
src={data.images[0]}
width={200}
height={200}
style={{ objectFit: "contain", padding: "20px" }}
/>
}
title={data.title || ""}
>
<Card.Meta
title={`Rs.${data.price}` || ""}
description={data.description || ""}
/>
</Card>
<Divider
style={{
marginTop: "10px",
marginBottom: "10px",
}}
/>
</div>
);
})}
</Container>
);
})}
<div className={"loader"} ref={observerElem}>
{isFetchingNextPage && hasNextPage ? (
<div
style={{
display: "flex",
alignItems: "center",
justifyContent: "center",
}}
>
<Spin />
</div>
) : (
<CenteredText>{"No search left"}</CenteredText>
)}
</div>
</CustomInfiniteScroll>
)}
</TableContainer>
);
};

The “ItemList” component returns JSX to render the UI. If the data is not available, a loading spinner is displayed. Otherwise, the data is mapped and rendered using the “Card” component from Ant Design. Each item is rendered with its image, title, price, and description. A “Divider” component is used to separate each item.

At the end of the component, a loading indicator is rendered while fetching the next page. If there are no more items to fetch, a message is displayed indicating the end of the search.

📺 Output:

Gif: showing the result of the infinite-scroll implementation

In this scenario, we observe that each time we scroll to the bottom of the screen, an API call is triggered. With every call, the page count increases, and the newly fetched items are subsequently appended to the original array.

🎉 Conclusion:

In this blog post, we demonstrate the implementation of a paginated infinite scroll using React-Query and Ant Design. We examined the key components, hooks, and techniques used to fetch and display data in a scrollable container. By understanding this code, you can apply similar concepts to implement infinite scrolling functionality in your own Next/Reactprojects.

Repo Link:https://github.com/RaazeshP96/react-infinite-scroll

Demo URL:https://react-infinite-scroll-six.vercel.app/

Thank you 🎉 🎉 🎉

--

--