Implement performant infinite scrolling in React using the Intersection Observer API without any third-party library.

Abdur Razzak
Full-Stack Web Developer
Infinite scroll loads more content as the user approaches the bottom of the page, providing a seamless browsing experience. It is commonly used in social feeds, product listings, and search results where paginated navigation would interrupt the flow. The Intersection Observer API is the modern, performant way to detect when an element enters the viewport — far better than scroll event listeners, which fire hundreds of times per second.
Create a ref for a sentinel element (an empty div) at the bottom of your list. In a useEffect, create an IntersectionObserver that fires a callback when the sentinel enters the viewport. Inside the callback, trigger your load-more function. Return a cleanup function that calls observer.disconnect() to prevent memory leaks. The observer fires once when the sentinel becomes visible, not continuously like a scroll event.
Encapsulate the IntersectionObserver logic in a custom hook that accepts a callback and returns a ref to attach to the sentinel element. The hook handles observer creation, cleanup, and the connection to the sentinel ref. Components that need infinite scroll just call useInfiniteScroll(loadMore) and attach the returned ref to a div at the bottom of their list.
Use TanStack Query's useInfiniteQuery for server-side paginated data. It manages the array of pages, the next page cursor, and loading states. The fetchNextPage function loads the next chunk of data and appends it to the existing pages. Flatten the pages array for rendering: data.pages.flatMap(page => page.items). Show a loading spinner when isFetchingNextPage is true.
Handle three edge cases: empty state (show an empty message when no items exist), end of data (hide the sentinel or show a 'You have reached the end' message when hasNextPage is false), and error state (show a retry button if fetchNextPage fails). Disable the observer when loading is in progress to prevent duplicate fetch calls from firing before the first request resolves.
For very long lists (thousands of items), implement virtual scrolling with @tanstack/react-virtual. Virtual scrolling renders only the items currently visible in the viewport, dramatically reducing DOM nodes and improving scroll performance. Combine virtual scrolling with infinite query for an efficient, scalable list that handles any amount of data without performance degradation.