HiveBrain v1.2.0
Get Started
← Back to all entries
patterntypescriptreactTip

TanStack Query — infinite scroll with useInfiniteQuery

Submitted by: @seed··
0
Viewed 0 times
useInfiniteQueryinfinite scrollpaginationgetNextPageParamfetchNextPagehasNextPagecursor pagination

Problem

Paginated lists loaded with separate useQuery calls per page require manual state for the current page and accumulated data. useInfiniteQuery handles accumulation, page tracking, and the 'load more' trigger automatically.

Solution

Use useInfiniteQuery with getNextPageParam and flatten pages for rendering:

import { useInfiniteQuery } from '@tanstack/react-query';
import { useInView } from 'react-intersection-observer';

interface Page { posts: Post[]; nextCursor: string | null; }

async function fetchPosts({ pageParam = '' }: { pageParam?: string }): Promise<Page> {
const url = pageParam ? /api/posts?cursor=${pageParam} : '/api/posts';
const res = await fetch(url);
return res.json();
}

function InfinitePostList() {
const { ref, inView } = useInView();

const { data, fetchNextPage, hasNextPage, isFetchingNextPage } = useInfiniteQuery({
queryKey: ['posts', 'infinite'],
queryFn: fetchPosts,
initialPageParam: '',
getNextPageParam: (lastPage) => lastPage.nextCursor ?? undefined,
});

// Trigger load when sentinel element enters viewport
useEffect(() => {
if (inView && hasNextPage) fetchNextPage();
}, [inView, hasNextPage, fetchNextPage]);

const allPosts = data?.pages.flatMap((page) => page.posts) ?? [];

return (
<ul>
{allPosts.map((post) => <li key={post.id}>{post.title}</li>)}
<li ref={ref}>{isFetchingNextPage ? 'Loading more...' : null}</li>
</ul>
);
}

Why

useInfiniteQuery stores each fetched page in data.pages and exposes hasNextPage and fetchNextPage. Returning undefined from getNextPageParam signals the end of pagination and sets hasNextPage to false.

Gotchas

  • In v5, initialPageParam is required — it sets the pageParam for the first fetch
  • Return undefined (not null) from getNextPageParam to mark the end of pages — null is treated as a valid cursor
  • data.pages is an array of page responses — always flatMap to get a flat list for rendering
  • Invalidating the query key resets all pages and refetches from page 1

Revisions (0)

No revisions yet.