Infinite Scroll with TanStack Query: Best Practices
Infinite Scroll Done Right with TanStack Query
Infinite scroll, that smooth, endless feed experience, is everywhere. Think social media feeds, product listings, you name it. It feels great for users, but implementing it well can be tricky. We want to avoid performance headaches and keep things responsive. Thankfully, TanStack Query (formerly React Query) makes this a whole lot simpler.
Let’s break down how to implement infinite scroll effectively using TanStack Query’s useInfiniteQuery hook. We’re aiming for a robust solution that handles loading states, errors, and fetching more data seamlessly.
The Core: useInfiniteQuery
The useInfiniteQuery hook is TanStack Query’s answer to paginated or infinite data. It’s designed to fetch data in chunks, manage the state of these chunks, and provide methods to fetch subsequent ones.
At its heart, useInfiniteQuery takes a query key and a query function. The query function receives an InfiniteData object which contains information about the current page. Crucially, it also has a pageParam property. This pageParam is what allows us to tell the API what data to fetch next.
Here’s a basic setup:
import { useInfiniteQuery } from '@tanstack/react-query';import axios from 'axios';
const fetchPosts = async ({ pageParam = 1 }) => { const { data } = await axios.get(`/api/posts?page=${pageParam}&limit=10`); return data;};
function PostList() { const { data, error, fetchNextPage, hasNextPage, isFetchingNextPage } = useInfiniteQuery({ queryKey: ['posts'], queryFn: fetchPosts, initialPageParam: 1, getNextPageParam: (lastPage, allPages) => { // Assuming your API returns a 'nextPage' property or similar // If there's no next page, return undefined if (!lastPage.nextPage) { return undefined; } return lastPage.nextPage; }, });
if (error) return 'An error has occurred: ' + error.message;
return ( <div> {data.pages.map((page, i) => ( <React.Fragment key={i}> {page.posts.map(post => ( <div key={post.id}>{post.title}</div> ))} </React.Fragment> ))} <button onClick={() => fetchNextPage()} disabled={!hasNextPage || isFetchingNextPage} > {isFetchingNextPage ? 'Loading more...' : hasNextPage ? 'Load More' : 'Nothing more to load'} </button> </div> );}Understanding the Key Parts
queryKey: A unique identifier for this query. TanStack Query uses this for caching and refetching.queryFn: Your actual data-fetching function. It receives an object withpageParam. Your function needs to use thispageParamto fetch the correct slice of data.initialPageParam: The starting point for yourpageParam. Usually1for page-based pagination or an offset like0.getNextPageParam: This is crucial. TanStack Query calls this function after a successful fetch. It receives thelastPage(the most recently fetched data) andallPages(an array of all fetched pages). Your job is to return thepageParamfor the next fetch. If there’s no next page, returnundefined. This tellsuseInfiniteQueryto stop fetching.
Enhancing the User Experience
The example above uses a button to trigger fetchNextPage. For true infinite scroll, we want this to happen automatically as the user scrolls.
We can achieve this by using the Intersection Observer API. We’ll attach a ref to a “sentinel” element – an element that sits at the bottom of our currently loaded content. When this sentinel enters the viewport, we trigger fetchNextPage.
import { useInfiniteQuery } from '@tanstack/react-query';import axios from 'axios';import React, { useEffect, useRef } from 'react';
const fetchPosts = async ({ pageParam = 1 }) => { const { data } = await axios.get(`/api/posts?page=${pageParam}&limit=10`); return data;};
function PostListInfinite() { const { data, error, fetchNextPage, hasNextPage, isFetchingNextPage } = useInfiniteQuery({ queryKey: ['posts'], queryFn: fetchPosts, initialPageParam: 1, getNextPageParam: (lastPage, allPages) => { if (!lastPage.nextPage) { return undefined; } return lastPage.nextPage; }, });
const observer = useRef(); const lastPostRef = React.useCallback(node => { if (isFetchingNextPage) return; if (observer.current) observer.current.disconnect(); observer.current = new IntersectionObserver(entries => { if (entries[0].isIntersecting && hasNextPage) { fetchNextPage(); } }); if (node) observer.current.observe(node); }, [isFetchingNextPage, hasNextPage, fetchNextPage]);
if (error) return 'An error has occurred: ' + error.message;
return ( <div> {data.pages.map((page, i) => ( <React.Fragment key={i}> {page.posts.map((post, postIndex) => ( <div key={post.id} ref={page.posts.length === postIndex + 1 && page.posts.length * (i + 1) === data.pages.reduce((sum, p) => sum + p.posts.length, 0) ? lastPostRef : null} // Attach ref to the last item of the last page > {post.title} </div> ))} </React.Fragment> ))} {isFetchingNextPage && <div>Loading more posts...</div>} </div> );}In this enhanced version:
- We use
useRefto create a ref for our sentinel element. lastPostRefis a callback ref that gets attached to the last item of the last fetched page. This element acts as our trigger.- An
IntersectionObserveris set up. When thelastPostRefelement enters the viewport (isIntersecting), and if there’s ahasNextPage, we callfetchNextPage(). - We conditionally render a “Loading more posts…” message when
isFetchingNextPageis true, providing visual feedback.
Key Considerations
- Error Handling: Always provide clear error messages to the user. TanStack Query makes
errorreadily available. - Loading States: Differentiate between the initial loading state and subsequent page fetches (
isFetchingNextPage). Show spinners or skeleton loaders accordingly. - API Design: Your backend API should support efficient pagination. Cursor-based pagination is often superior to offset/page-based for true infinite scrolling, as it avoids issues with data shifting.
- Performance: TanStack Query handles caching and deduplication automatically, which is a massive performance win. However, be mindful of how much data you’re rendering on the client-side at once.
Implementing infinite scroll doesn’t have to be a chore. With useInfiniteQuery and a touch of Intersection Observer magic, you can build smooth, performant infinite scrolling experiences that users will love. It’s a powerful pattern when done right.