SyntaxStudy
Sign Up
React Pagination and Infinite Scroll with TanStack Query
React Beginner 1 min read

Pagination and Infinite Scroll with TanStack Query

TanStack Query provides useInfiniteQuery for paginated data that users scroll through continuously. The hook manages multiple pages of data as a single cache entry. The getNextPageParam function receives the last page of data and returns the parameter to use for the next page request, or undefined to signal that there are no more pages. The data object returned by useInfiniteQuery contains a pages array — one entry per fetched page — and a pageParams array. You flatten pages into a single list for rendering. A sentinel element at the bottom of the list combined with an Intersection Observer triggers fetchNextPage when it enters the viewport, producing smooth infinite scroll without polling or manual scroll-event listeners. For classic numbered pagination, useQuery with a page state variable and keepPreviousData: true provides a seamless experience: the previous page stays visible while the next page loads, preventing jarring blank states between page changes.
Example
import { useInfiniteQuery } from '@tanstack/react-query';
import { useRef, useEffect } from 'react';

async function fetchPage({ pageParam = 1 }) {
    const res = await fetch(`/api/posts?page=${pageParam}&limit=10`);
    if (!res.ok) throw new Error('Fetch failed');
    return res.json(); // { data: [], nextPage: number | null }
}

function InfinitePostList() {
    const {
        data,
        fetchNextPage,
        hasNextPage,
        isFetchingNextPage,
        isLoading,
    } = useInfiniteQuery({
        queryKey:        ['posts'],
        queryFn:         fetchPage,
        getNextPageParam: lastPage => lastPage.nextPage ?? undefined,
    });

    // Intersection Observer sentinel
    const sentinelRef = useRef(null);
    useEffect(() => {
        const el = sentinelRef.current;
        if (!el || !hasNextPage) return;
        const observer = new IntersectionObserver(([entry]) => {
            if (entry.isIntersecting) fetchNextPage();
        });
        observer.observe(el);
        return () => observer.unobserve(el);
    }, [hasNextPage, fetchNextPage]);

    if (isLoading) return <p>Loading…</p>;

    const posts = data.pages.flatMap(page => page.data);

    return (
        <ul>
            {posts.map(post => (
                <li key={post.id}>{post.title}</li>
            ))}
            <li ref={sentinelRef}>
                {isFetchingNextPage ? 'Loading more…' : hasNextPage ? '' : 'All loaded'}
            </li>
        </ul>
    );
}

This is the last lesson in this section.

Create a free account to earn a certificate