React cookbook · Intermediate
Build infinite scroll with TanStack Query
Load more items automatically as the user scrolls a long feed, using cursor pagination.
Approach
Use useInfiniteQuery with the API's next_cursor as getNextPageParam, and an IntersectionObserver on a sentinel element at the bottom of the list to call fetchNextPage(). Keep a visible "Load more" button as an accessible fallback.
Solution
import { useEffect, useRef } from "react";
import { useInfiniteQuery } from "@tanstack/react-query";
type Page = { items: Post[]; next_cursor: string | null };
export function Feed() {
const { data, fetchNextPage, hasNextPage, isFetchingNextPage, status } = useInfiniteQuery({
queryKey: ["feed"],
queryFn: async ({ pageParam, signal }) => {
const url = pageParam ? `/api/feed?cursor=${encodeURIComponent(pageParam)}` : "/api/feed";
const res = await fetch(url, { signal });
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return (await res.json()) as Page;
},
initialPageParam: null as string | null,
getNextPageParam: (last) => last.next_cursor,
});
const sentinel = useRef<HTMLDivElement>(null);
useEffect(() => {
const el = sentinel.current;
if (!el || !hasNextPage) return;
const observer = new IntersectionObserver(([entry]) => {
if (entry.isIntersecting && !isFetchingNextPage) fetchNextPage();
}, { rootMargin: "400px" });
observer.observe(el);
return () => observer.disconnect();
}, [hasNextPage, isFetchingNextPage, fetchNextPage]);
if (status === "pending") return <p>Loading…</p>;
if (status === "error") return <p role="alert">Couldn't load the feed.</p>;
return (
<>
<ul>{data.pages.flatMap((p) => p.items).map((post) => <li key={post.id}>{post.title}</li>)}</ul>
<div ref={sentinel} />
{hasNextPage && (
<button onClick={() => fetchNextPage()} disabled={isFetchingNextPage}>
{isFetchingNextPage ? "Loading…" : "Load more"}
</button>
)}
</>
);
}
Pitfalls
- Offset-based pages on a changing feed (duplicates/skips).
- Rendering thousands of DOM nodes — combine with virtualization for very long lists.
- No keyboard-accessible way to load more.
Variations
- Use
maxPagesto cap memory for very long sessions. - Restore scroll position when navigating back (keep the query cached).
Further reading: TanStack Query: Infinite queries
Reference: https://tanstack.com/query/latest/docs/framework/react/guides/infinite-queries
More React recipes
- Add optimistic updates to a todo list
- Build an accessible modal dialog
- Call a cookie-authenticated API with CSRF protection
- Debounce a search input in React
- Handle errors gracefully with error boundaries
- Implement dark mode that follows the system
- Poll a long-running job's status
- Protect routes in a React SPA