How We Cut Astro Build Time from 30 Minutes to 5 Minutes (⚡ 83% Faster!)
Pagination is a common technique used to enhance performance and user experience when developing online apps that display vast volumes of data. While a traditional pagination provides structure, it often disrupts the flow of navigation. Users are forced to stop, click, and wait for the next set of result.
Infinite scroll, on the other hand, offers a contemporary kind of pagination. It allows endless scrolling, enabling users to browse through content without having to click on pagination links. To make this experience efficient and maintainable, developers need the right tooling.
React-Query v3 is a powerful library that simplifies data-fetching and state management in React applications, making it an ideal choice for implementing infinite scroll pagination.
In this blog post, we’ll look at how to build infinite scroll using React-Query (now TanStack Query) and TypeScript in a React application to improve user experience and handle paginated data.
Core Tools for Building Infinite Scroll
1. React-Query
React-Query is a powerful data-fetching and state management library for React. One of its notable features is the useInfiniteQuery hook, which simplifies the process of implementing infinite scroll pagination in React applications. In this tutorial, we will focus on using useInfiniteQuery to fetch and paginate data from an API in our React app.
2. Intersection Observer
IntersectionObserver is a web API that allows efficient detection of when an element is visible in the viewport or intersects with another element.
We will be using IntersectionObserver to implement pagination in our application.
- We can create a ref for the last element in the list, and then use IntersectionObserver to observe this element.
- When the last element becomes visible in the viewport, we can fetch more data to implement infinite scroll pagination.
This allows us to efficiently load more data as the user scrolls, providing a smooth and optimized pagination experience.
Using Infinite Scroll with React-Query v3
Let us start by creating a react app.
yarn create vite infinite-scroll-app --template react-ts
To use infinite scroll pagination with React-Query v3, you’ll need to install the library and set up your query. First, install React-Query v3 and axios, a popular library for making HTTP requests, using npm or yarn:
yarn add react-query axios
Once you have installed React-Query v3 and axios, the next step is to set up your query and implement the infinite scroll logic. Before diving into that, let’s clean up the App.tsx file by removing all unnecessary code.
//App.tsx
import './App.css';
function App() {
return <div>Infinate Scroll App</div>;
}
export default App;
Then, we need to set up a QueryClientProvider at the top level of our application to provide the QueryClient instance to all the child components:
//main.tsx
import React from 'react';
import ReactDOM from 'react-dom/client';
import { QueryClient, QueryClientProvider } from 'react-query';
import App from './App';
import './index.css';
const queryClient = new QueryClient();
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
<QueryClientProvider client={queryClient}>
<React.StrictMode>
<App />
</React.StrictMode>
</QueryClientProvider>
);
We will now create a function to fetch data. To simulate pagination, we will use the PokeAPI as our endpoint.
//utils/fetchData.ts
import axios from 'axios';
import { QueryFunction } from 'react-query';
const LIMIT = 10
export interface ItemDataI {
name: string;
}
interface APIResultsI {
results: ItemDataI[];
offset: number | null;
}
const fetchData: QueryFunction<APIResultsI, 'pokemon'> = async ({
pageParam,
}) => {
const offset = pageParam ? pageParam : 0;
const data = await axios.get(
`https://pokeapi.co/api/v2/pokemon?offset=${offset}&limit=${LIMIT}`
);
return {
results: data.data.results,
offset: offset + LIMIT,
};
};
export default fetchData;
Next, let’s implement infinite scrolling. Start by implementing React-Query’s useInfiniteQuery hook.
//App.tsx
const {
data,
error,
fetchNextPage,
hasNextPage,
isFetching,
isLoading,
} = useInfiniteQuery('data', fetchData, {
getNextPageParam: (lastPage, pages) => lastPage.offset,
});
The useInfiniteQuery hook returns an object containing the following properties:
data: the data returned by ourfetchDatafunctionerror: any errors returned byfetchDatafunctionfetchNextPage: a function that fetches next set of datahasNextPage: a boolean indicating whether there is more data to fetchisFetching: a boolean indicating whether data is currently being fetchedisLoading: a boolean indicating whether the query is currently loading. This will betruefor only the initial load.
Additionally, the useInfiniteQuery hook includes a getNextPageParam function. This function determines the value of the pageParam parameter for the next page of data to fetch.
When using useInfiniteQuery from React-Query to fetch data, the returned data is usually an array of pages, where each page contains an array of items. To easily map through and render these items, we will flatten the data into a single array. We can use the useMemo hook to efficiently flatten the data and prevent unnecessary recalculations.
//App.tsx
const flattenedData = useMemo(
() => (data ? data?.pages.flatMap(item => item.results) : []),
[data]
);
To implement infinite scroll pagination, we need to create a reference for the last element in the list and set up an IntersectionObserver to detect when that element becomes visible on the screen. Once the last element is visible, we can trigger the fetchNextPage function to fetch more data.
//App.tsx
const observer = useRef<IntersectionObserver>();
const lastElementRef = useCallback(
(node: HTMLDivElement) => {
if (isLoading) return;
if (observer.current) observer.current.disconnect();
observer.current = new IntersectionObserver(entries => {
if (entries[0].isIntersecting && hasNextPage && !isFetching) {
fetchNextPage();
}
});
if (node) observer.current.observe(node);
},
[isLoading, hasNextPage]
);
Finally, we render the list of items.
//App.tsx
if (isLoading) return <h1>Loading Data</h1>;
if (error) return <h1>Couldn't fetch data</h1>;
return (
<div>
<div>
{flattenedData.map((item, i) => (
<div
key={i}
ref={flattenedData.length === i + 1 ? lastElementRef : null}>
<p>{item.name}</p>
</div>
))}
</div>
{isFetching && <div>Fetching more data</div>}
</div>
);
We can use the isLoading and error returned by useInfiniteQuery to display loading and error messages, respectively.
To render the list, we simply map through the flattened data and return each item. We also attach lastElementRef to the last element in the list. This allows our IntersectionObserver to observe the last element and trigger fetchNextPage when it becomes visible on the screen.
And with that, we’ve successfully implemented a smooth infinite scroll using React-Query v3 and IntersectionObserver, providing a seamless pagination experience for your users.
You can find the GitHub repository for the entire code here and StackBlitz.
Conclusion
In summary, implementing infinite scroll pagination with React-Query v3 and IntersectionObserver is a simple yet powerful feature that enhances the performance and user experience of your React applications.
By leveraging the capabilities of React-Query for data fetching and state management, and utilizing the built-in IntersectionObserver API for detecting when to fetch more data, you can create a smooth and efficient pagination implementation without the need for additional external libraries or complex logic.
This approach results in a seamless and responsive browsing experience for your users, making it a valuable addition to your web applications.
Useful links
FAQs
1. What advantages does React-Query’s useInfiniteQuery provide over a custom implementation?
useInfiniteQuery abstracts away common concerns like managing page state, caching responses, handling loading/error states, and detecting if more data is available. This lets developers focus on rendering and user experience instead of boilerplate data-fetching logic.
2. How does Intersection Observer improve performance compared to scroll event listeners?
Unlike scroll listeners that fire continuously and can degrade performance, Intersection Observer is event-driven. It efficiently notifies you only when a target element enters or leaves the viewport, reducing reflows and improving responsiveness.
3. Can I combine infinite scroll with manual “Load More” buttons?
Yes. You can still use fetchNextPage manually with a button, while also setting up Intersection Observer for auto-loading. This hybrid approach gives users flexibility and keeps accessibility in mind.





