How would you implement infinite scrolling in React?
Use IntersectionObserver on a sentinel element to trigger the next page fetch. Track cursor + loading + hasMore in state, dedupe in-flight requests, and virtualize once rendered rows exceed a few thousand.
Infinite scroll trades pagination clicks for "fetch the next page when the user nears the bottom." It looks simple and breaks in subtle ways: duplicate fetches, scroll position jumps, memory bloat, broken back-button behavior. A senior answer covers the trigger mechanism, request bookkeeping, list growth strategy, and accessibility/SEO trade-offs.
Trigger: IntersectionObserver, not scroll listeners. Attach an observer to a sentinel element placed just below the last item. When it intersects the viewport, fetch the next page. This is O(1) per scroll, runs off the main thread for hit-testing, and avoids the throttle/debounce dance you'd need with a scroll listener. Scroll listeners fire 60+ times per second and force you to read scrollTop / layout, which thrashes.
State shape. The minimum is { items, cursor, isLoading, hasMore, error }. Use a server cursor (opaque token) — not an offset — so concurrent inserts on the server don't cause duplicates or skips. With TanStack Query, useInfiniteQuery gives you all of this with cache, retry, and dedupe baked in.
Dedupe in-flight requests. Without guarding, a fast scroller can fire the same "page 2" fetch three times. Either: (a) early-return when isLoading is true, (b) abort the previous request with AbortController when a new one starts, or (c) let the query cache key (cursor) dedupe automatically. useInfiniteQuery does (c) for free.
hasMore + end state. When the API returns an empty page or a null next cursor, set hasMore = false and stop observing the sentinel. Render an explicit "You've reached the end" — silent stops feel broken.
Memory: rendered nodes grow unbounded. After ~2000 items the DOM becomes the bottleneck. Two options: (1) virtualize with @tanstack/react-virtual so only viewport rows mount; (2) window older items by unmounting pages above the viewport. Virtualization is cleaner because it preserves scroll position and works with variable-height rows.
Scroll restoration. When the user clicks an item, navigates away, and hits back, they expect to land at the same scroll offset with the same data already loaded. Strategies: (a) cache pages in TanStack Query / SWR with staleTime > 0 so the back-nav re-renders instantly; (b) save scrollY on unmount, restore on mount; (c) Next.js handles much of this with the App Router cache.
Accessibility & SEO. Infinite scroll hides content from keyboard users (no way to reach the footer) and crawlers. Mitigations: pair with a "Load more" button as the keyboard-accessible trigger (the observer + button can coexist), expose pagination links for SEO, and use aria-busy on the list during fetches. Many teams reserve infinite scroll for feeds and use cursor pagination with explicit "Next" elsewhere.
Errors and retries. A failed fetch shouldn't permanently break scroll. Show an inline retry CTA on the sentinel; the next intersection or click re-attempts. Don't silently retry forever — exponential backoff with a cap.
Code
Follow-up questions
- •How does TanStack Query's useInfiniteQuery dedupe and cache pages?
- •When would you choose 'Load more' over auto-trigger?
- •How do you preserve scroll position across route navigation?
- •How do you combine infinite scroll with virtualization?
Common mistakes
- •Using a scroll listener instead of IntersectionObserver — perf cost and complex math.
- •Using offset-based pagination — duplicates/skips when items are inserted server-side.
- •Forgetting to unobserve when hasMore becomes false — observer keeps firing.
- •No in-flight guard — fast scrolling triggers the same fetch multiple times.
- •Letting the DOM grow to 10k+ rows — jank on every scroll/render.
Performance considerations
- •rootMargin: '400px' pre-fetches before the user hits the bottom — feels seamless.
- •Virtualize after ~1–2k rendered items.
- •AbortController on the previous fetch when a new one starts.
Edge cases
- •Server returns same cursor twice → infinite loop. Detect and stop.
- •User scrolls fast past the sentinel — observer may fire only once; ensure rootMargin is generous.
- •Window resize changes the sentinel position — observer handles this automatically.
Real-world examples
- •Twitter, Instagram, LinkedIn feeds — all cursor-paginated infinite scroll with virtualization.