Infinite scrolling has become a common feature in most modern web applications. We can see that instead of showing a pagination and next page button, the new content is automatically loading as we scroll down through the content. We can see this behavior on most of the social media applications, e-commerce websites, and other content platforms. This has become a more engaging and user-friendly technique.
In a React application, implementing this infinite scroll feature and manually handling the pagination, loading states, and data fetching can be challenging and hard to maintain. So TanStack Query is the perfect solution for this. In this blog, we can learn and build an infinite scrolling list using React and TanStack Query.
What is Infinite Scroll?
Infinite scroll is a UI pattern where the next page data is automatically loaded when the user reaches the bottom of the page or the container. There are no additional UI clicks for loading new data. This creates a smooth user experience and reduces unnecessary user interactions.
Initial Setup
First, set up a React application if you don't have one.
npm create vite@latest my-app
cd my-app
npm install
Install the tanstack query package
npm install @tanstack/react-query
Now, we need to wrap our application with a query client so we can use TanStack Query hooks.
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { Users } from "./Users";
const queryClient = new QueryClient();
function App() {
return (
<QueryClientProvider client={queryClient}>
<Users />
</QueryClientProvider>
);
}
export default App;Creating the Fetch function
For this example, we can use this API for users' data.
https://randomuser.me/api/?page=1&results=10
Now, we can create a function to fetch the users.
const fetchUsers = async ({ pageParam = 1 }) => {
const response = await fetch(
`https://randomuser.me/api/?page=${pageParam}&results=10`,
);
return response.json();
};Using useInfiniteQuery
Now, we can configure an infinite query using the useInfiniteQuery hook of TanStack Query.
import { useInfiniteQuery } from "@tanstack/react-query";
const {
data,
fetchNextPage,
hasNextPage,
isFetchingNextPage,
isPending,
isError,
} = useInfiniteQuery({
queryKey: ["users"],
queryFn: fetchUsers,
initialPageParam: 1,
getNextPageParam: (lastPage, allPages) => {
return allPages.length + 1;
},
});- queryKey - this key is used by TanStack Query for caching
- queryFn - this function is called to fetch data from
- initialPageParam - sets the initial page value as this
- getNextPageParam - use this function to determine which page should be loaded next
Rendering the Data
The data that is returned by useInfiniteQuery contains multiple pages. So, we need to loop through all pages and render the user details.
if (isPending) {
return <p>Loading...</p>;
}
return (
<div>
{data.pages.map((page, pageIndex) => (
<div key={pageIndex}>
{page.results.map((user) => (
<div key={user.login.uuid}>
<h3>
{user.name.first} {user.name.last}
</h3>
</div>
))}
</div>
))}
</div>
);Detecting When Users Reach the Bottom
Now, we can set up a method to detect if the user reaches the bottom to trigger the fetching of the next page.
import { useRef, useEffect } from "react";
const loadMoreRef = useRef(null);
useEffect(() => {
const observer = new IntersectionObserver(
(entries) => {
if (
entries[0].isIntersecting &&
hasNextPage &&
!isFetchingNextPage
) {
fetchNextPage();
}
},
{
threshold: 1,
}
);
if (loadMoreRef.current) {
observer.observe(loadMoreRef.current);
}
return () => observer.disconnect();
}, [fetchNextPage, hasNextPage, isFetchingNextPage]);Adding the Trigger Element
<div ref={loadMoreRef}>
{isFetchingNextPage ? "Loading more..." : ""}
</div>;Now, when a user scrolls to this element, the TanStack Query can fetch the next page.
Handling Errors
Add this code to handle API errors gracefully.
if (isError) {
return <p>Something went wrong.</p>;
}Full code
import { useInfiniteQuery } from "@tanstack/react-query";
import { useEffect, useRef } from "react";
const fetchUsers = async ({ pageParam = 1 }) => {
const response = await fetch(
`https://randomuser.me/api/?page=${pageParam}&results=10`,
);
return response.json();
};
export function Users() {
const {
data,
fetchNextPage,
hasNextPage,
isFetchingNextPage,
isPending,
isError,
} = useInfiniteQuery({
queryKey: ["users"],
queryFn: fetchUsers,
initialPageParam: 1,
getNextPageParam: (lastPage, allPages) => {
return allPages.length + 1;
},
});
const loadMoreRef = useRef(null);
useEffect(() => {
const observer = new IntersectionObserver(
(entries) => {
if (entries[0].isIntersecting && hasNextPage && !isFetchingNextPage) {
fetchNextPage();
}
},
{
threshold: 1,
},
);
if (loadMoreRef.current) {
observer.observe(loadMoreRef.current);
}
return () => observer.disconnect();
}, [fetchNextPage, hasNextPage, isFetchingNextPage]);
if (isPending) {
return <div>Loading...</div>;
}
if (isError) {
return <div>Something went wrong.</div>;
}
return (
<div>
{data.pages.map((page, pageIndex) => (
<div key={pageIndex}>
{page.results.map((user) => (
<div key={user.login.uuid}>
<h3>
{user.name.first} {user.name.last}
</h3>
</div>
))}
</div>
))}
<div ref={loadMoreRef}>
{isFetchingNextPage ? "Loading more..." : ""}
</div>
</div>
);
}Infinite scrolling can improve the user experience while browsing large datasets. However, implementing it manually can become hard to maintain. TanStack Query can help in this case and simplify the entire process with useInfiniteQuery. We can combine this with the Intersection Observer API to build a smooth and performant infinite scrolling experience.
To read more about How to Implement Optimistic Updates in React with TanStack Query, refer to our blog How to Implement Optimistic Updates in React with TanStack Query.