Feeds, search results, and comment threads all share a pattern: accumulating pages of data as a user scrolls, rather than replacing what's already loaded. This lesson covers useInfiniteQuery, getNextPageParam, fetchNextPage, and wiring it up to true infinite scroll with an IntersectionObserver.
1Beyond a Single Page of Results
Feeds, search results, and comment threads share an accumulating pagination pattern: load the first page, then append additional pages as the user scrolls, rather than replacing existing data. useInfiniteQuery is purpose-built for this shape, distinct from useQuery's single fetch-per-key model.
2The getNextPageParam Function
useInfiniteQuery needs a getNextPageParam function that receives the most recently fetched page and returns whatever parameter (a cursor, a page number) should be passed to queryFn for the next request, or undefined to signal there are no more pages.
3Fetching the Next Page with fetchNextPage
useInfiniteQuery returns data.pages (every fetched page accumulated so far), a fetchNextPage function to trigger loading another page, and a hasNextPage boolean derived from getNextPageParam's last return value β enough to drive a 'Load More' button rendering all accumulated pages.
4Triggering fetchNextPage on Scroll
For true infinite scroll without a button, fetchNextPage can be paired with an IntersectionObserver watching a sentinel element near the bottom of the list, automatically triggering the next page fetch when it enters the viewport, guarded against duplicate or unnecessary calls.
5Step-by-Step Breakdown
Beyond a Single Page of Results. A social feed, a search results list, or a comment thread all share a pattern: load the first page, then load more as the user scrolls, appending to what's already there β not replacing it. useQuery alone fetches one thing per key; useInfiniteQuery is built specifically for this accumulating, paginated shape.
The getNextPageParam Function. useInfiniteQuery needs to know how to fetch the NEXT page from a given response β getNextPageParam receives the last-fetched page and returns whatever value (a cursor, a page number) should be passed to queryFn for the next request, or undefined if there are no more pages.
What does returning undefined from getNextPageParam signal to useInfiniteQuery?
- βThere are no more pages left to fetch
- βThe last request failed and should be retried
Fetching the Next Page with fetchNextPage. useInfiniteQuery returns data.pages (an array of every fetched page so far), fetchNextPage (a function to trigger loading the next one), and hasNextPage (a boolean derived from whether getNextPageParam last returned a real value). Call fetchNextPage from an 'Load More' button or an intersection observer.
Triggering fetchNextPage on Scroll. For true infinite scroll (no button), pair fetchNextPage with an IntersectionObserver watching a sentinel element at the bottom of the list β when it enters the viewport, call fetchNextPage() automatically, guarded by hasNextPage and !isFetchingNextPage to avoid duplicate calls.
Why guard the scroll-triggered fetchNextPage() call with hasNextPage && !isFetchingNextPage?
- βTo avoid firing duplicate requests or fetching past the last page
- βIt's purely a CSS/styling concern
Mastery Achieved. You now understand infinite queries: useInfiniteQuery for accumulating paginated data, getNextPageParam for determining what comes next (and when to stop), fetchNextPage/hasNextPage for driving a load-more button, and pairing it with an IntersectionObserver for true infinite scroll. This closes out Advanced Data Fetching β next, you'll move into Advanced Forms.
Level Up π
Advanced cheat sheets, SEO tricks, and interview prep for this topic.
Browser Support
IntersectionObserver is fully supported in all modern browsers.
Fully supported.
Fully supported.
Fully supported.
Accessibility (A11y)
1Infinite Scroll Needs an Alternative for Keyboard and Screen Reader Users
Pure scroll-triggered loading can be hard to use for keyboard-only or screen reader users who don't scroll the same way sighted mouse users do β pairing infinite scroll with a visible, focusable 'Load More' button as a fallback keeps the experience accessible.
2Announce Newly Loaded Content
When a new page of items loads via infinite scroll, consider an aria-live region announcing that new content was added, so screen reader users are aware content beyond what they've already heard exists.
SEO Implications
- 1
Infinite Scroll Content May Not Be Crawlable Without Additional Handling
Content loaded only via client-side scroll-triggered fetches after the initial page load may not be discovered by crawlers that don't scroll or execute the triggering JavaScript β pair infinite scroll with real paginated URLs or ensure critical content is present in the initial server-rendered response for SEO-sensitive pages.
Best Practices
Always Guard fetchNextPage Calls Against Duplicate Triggers
Check both hasNextPage and !isFetchingNextPage before calling fetchNextPage from a scroll observer, since intersection events can fire multiple times in quick succession.
Provide a Manual 'Load More' Fallback Alongside Scroll Triggers
Even with automatic infinite scroll, a visible, keyboard-accessible button gives users control and a fallback if the observer-based triggering doesn't fire as expected.
Frequent Bugs
Scrolling near the bottom of a list triggers several duplicate page fetches in quick succession.
The IntersectionObserver callback isn't checking hasNextPage and isFetchingNextPage before calling fetchNextPage, allowing multiple overlapping calls. Add both checks before triggering the next fetch.
The list stops loading more items even though the API clearly has more data available.
getNextPageParam is likely returning undefined too early, or not correctly extracting the next cursor/page number from the API's actual response shape. Verify it matches the real pagination metadata returned by the endpoint.
Real-World Examples
An Infinite-Scrolling Comment Thread
A comment section needs to load additional comments as the user scrolls down, accumulating them rather than replacing the list. useInfiniteQuery with a cursor-based getNextPageParam, combined with an IntersectionObserver on a sentinel div at the bottom of the rendered comments, provides seamless infinite scroll.
const { data, fetchNextPage, hasNextPage, isFetchingNextPage } = useInfiniteQuery({
queryKey: ['comments', postId],
queryFn: ({ pageParam }) => fetchComments(postId, pageParam),
initialPageParam: null,
getNextPageParam: (lastPage) => lastPage.nextCursor,
});
{data.pages.flatMap(page => page.comments).map(c => <Comment key={c.id} {...c} />)}
<div ref={sentinelRef} />