In the previous React Code Interview issues, we explored the Intersection Observer API and modified our API to support pagination when scrolling.
However, making infinite scrolling work perfectly is trickier than it seems. Once we fetch data from a remote server, several challenges arise:
Race conditions – What if an older request returns later than a newer one, overwriting fresh data?
Error handling – What if a request fails or never returns?
State management – Are there more items to load? Have we already reached the bottom? Is there an ongoing request we should wait for?
Handling these manually is possible but tedious—especially when working on multiple features requiring infinite scrolling. Wouldn't it be great if a tool could take care of all these mundane details for us?
That’s where React Query(TanStack Query) comes in.
Instead of manually managing API calls, state updates, and caching, React Query automates the process, keeping our code clean and our UI smooth.
Why Use React Query?
✅ Simplifies API Calls – Handles pagination, caching, and background fetching out of the box.
✅ Optimized Performance – Prevents redundant requests and speeds up UI interactions.
✅ Robust Error Handling – Gracefully manages failures and retries failed requests.
✅ Seamless UX – Preloads data to ensure a smooth scrolling experience.
This approach is widely used in real-world applications (social feeds, e-commerce, dashboards) and is a common front-end interview challenge. Let’s see it in action.
Core Code Snippet
Here’s how we enhance infinite scrolling using React Query’s useInfiniteQuery
, while keeping Intersection Observer for trigger detection.
function App() {
const ref = useRef<HTMLDivElement | null>(null);
const triggerRef = useRef<HTMLLIElement | null>(null);
const { data, hasNextPage, fetchNextPage } =
useInfiniteQuery<PaginatedQuotes>({
queryKey: ["quotes"],
queryFn: ({ pageParam = 1 }) => fetchQuotes(pageParam),
initialPageParam: 1,
getNextPageParam: (lastPage: PaginatedQuotes) =>
lastPage.meta.hasMore ? lastPage.meta.currentPage + 1 : undefined,
});
useEffect(() => {
if (!ref.current) return;
const observer = new IntersectionObserver((entries) => {
if (entries[0].isIntersecting && hasNextPage) {
fetchNextPage();
}
}, { root: ref.current, threshold: 1 });
if (triggerRef.current) {
observer.observe(triggerRef.current);
}
return () => observer.disconnect();
}, [hasNextPage]);
return (
<div className="container" ref={ref}>
<ol>
{data?.pages?.map((page, index) => (
<Fragment key={index}>
{page.quotes.map((quote) => (
<li key={quote.id} className="list-item">
{quote.text} - {quote.author}
</li>
))}
</Fragment>
))}
<li className="trigger" ref={triggerRef}>
{hasNextPage ? "Load more..." : ""}
</li>
</ol>
</div>
);
}
export default App;
Breaking It Down
1️⃣ useInfiniteQuery
for Auto Pagination
Instead of manually tracking pages, useInfiniteQuery
does the heavy lifting:
queryFn → Fetches the next page of quotes.
getNextPageParam → Determines whether more pages exist.
hasNextPage → React Query handles this automatically.
No more manual state updates—we simply call fetchNextPage()
when needed.
Keep reading with a 7-day free trial
Subscribe to The Pragmatic Developer to keep reading this post and get 7 days of free access to the full post archives.