renderpx
Theme: auto

Infinite Scroll

Load feed (or list) data in pages as the user scrolls, instead of loading everything upfront.

The problem I keep seeing

Long feeds (social timelines, product grids, activity logs) can’t load all items at once: slow initial load, huge DOM, and wasted bandwidth. You need pagination. The UX question is whether to use a “Load more” button or to load automatically when the user scrolls near the bottom—infinite scroll.

Infinite scroll is familiar from Twitter, Instagram, and many dashboards. The implementation pitfalls: cursor vs offset pagination, avoiding duplicate or missing items when the list changes, and (if the list is very long) combining with virtualization so you don’t mount thousands of DOM nodes.

Naive approach

Fetch the whole list with a large limit. Works only for small datasets.

tsx
Loading...

First improvement

Paginate: fetch page 1, then page 2, etc., and append to state. Use a “Load more” button so the user explicitly requests the next page. This fixes memory and DOM size; the only downside is the extra click for “infinite” feel.

tsx
Loading...

Why this helps: You only render what you’ve loaded. Backend can use LIMIT/OFFSET or cursor-based pagination.

Remaining issues

  • Offset pagination: page=2 can skip or duplicate items if the list changes between requests (e.g. new item inserted at top). Cursor-based (e.g. after=id_xyz) is stable.
  • No automatic load on scroll: Users expect scrolling to the bottom to load more. You need a sentinel element and IntersectionObserver (or a library that does it).
  • Caching and deduplication: React Query’s useInfiniteQuery handles page caching and gives you fetchNextPage and hasNextPage out of the box.

Production pattern

Use useInfiniteQuery with cursor-based pagination. The API returns items and nextCursor; getNextPageParam tells React Query how to request the next page. Add a sentinel at the bottom and call fetchNextPage when it becomes visible.

tsx
Loading...
Sentinel hook for auto-load on scrolltsx
Loading...

When I use this

  • Use: Feeds, timelines, product grids, activity logs—any long list where the user scrolls to consume content. Prefer cursor-based pagination for feeds that update in real time.
  • Consider “Load more” instead: When you need a stable scroll position for accessibility (e.g. “Back to top” or predictable focus). Infinite scroll can make it hard to reach footer or repeat a “load more” action.
  • Combine with virtualization: If the list has thousands of items in the DOM (even loaded in chunks), use a virtualized list so only visible rows are mounted.

A11y

Infinite scroll is hard for keyboard and screen-reader users. For critical lists, offer a “Load more” button or pagination as well.

Gotchas

  • Root margin: With IntersectionObserver, use rootMargin: '200px' (or similar) so the next page starts loading before the user actually hits the bottom.
  • Stale closures: Pass fetchNextPage, hasNextPage, and isFetchingNextPage into the effect deps so the observer uses current values.
  • Duplicate keys: Flatten pages with flatMap; ensure each item has a stable key (id). If the API can return the same item in two pages during rebalancing, dedupe by id.

Data Fetching & Sync → · All patterns