Infinite scroll loads the next page automatically as the user nears the bottom — no "Load more" button. The modern, jank-free way to detect "near the bottom" is a sentinel: an empty element placed at the end of the list, watched by an IntersectionObserver. When the sentinel scrolls into view, you fetch the next page. The catch is guarding against duplicate loads — the sentinel can fire repeatedly while a fetch is already in flight.
Implement useInfiniteScroll({ hasMore, loading, onLoadMore, rootMargin }). Return { sentinelRef }, a callback ref for the sentinel element. When it intersects and hasMore and not loading, call onLoadMore(). Use rootMargin to trigger early (before the sentinel is fully visible).
function useInfiniteScroll({ hasMore, loading, onLoadMore, rootMargin = '0px' }) {
// returns { sentinelRef }
}
const { sentinelRef } = useInfiniteScroll({
hasMore, loading, onLoadMore: fetchNextPage, rootMargin: '200px',
});
<>
{items.map((it) => <Row key={it.id} {...it} />)}
<div ref={sentinelRef} /> {/* fetches when this scrolls near view */}
</>
IntersectionObserver.onLoadMore() only when the sentinel isIntersecting and hasMore and !loading.loading flag stops repeated intersects from firing a second fetch mid-request.hasMore/loading/onLoadMore from a ref so it never acts on stale props. Disconnect on ref change / unmount; rootMargin lets you prefetch early.Unlock the solution & editor
Runnable editor + tests
Solve in the browser with instant Jest feedback.
Detailed solutions
Walkthroughs, edge cases, and complexity notes.
Multi-framework variants
React, Vue, Vanilla, Angular — same question, different stacks.