Back to React
React
medium
mid

How do you manage state for pagination and loading indicators in a React app?

Track page/cursor, page size, items, total/hasMore, plus discrete status (idle/loading/success/error). Distinguish initial load (skeleton) from page change (spinner) from background refetch. Offset vs cursor pagination. A query library handles most of this; know what it's doing.

5 min read·~8 min to think through

Pagination state is more than "current page" — and the loading indicator isn't one boolean.

The state you actually need

js
{
  page,        // or cursor — current position
  pageSize,
  items,       // current page's data (or accumulated, for infinite scroll)
  total,       // or hasMore / nextCursor
  status,      // 'idle' | 'loading' | 'success' | 'error' — not a boolean
}

Use a status enum, not isLoading: boolean. A boolean can't express "loaded but now refetching" or "error but showing stale data." A state machine (idle → loading → success/error) is clearer and prevents impossible states.

Three kinds of loading — show them differently

  1. Initial load — no data yet → skeleton screen for the whole list.
  2. Page change — switching pages → spinner on the list, or disable pagination controls, often keep the old page visible until the new one arrives.
  3. Background refetch — stale data being revalidated → a subtle indicator (a small spinner in the corner), keep showing current data.

Conflating these — one big spinner for everything — is the common mistake.

Offset vs cursor pagination

  • Offset (?page=2&size=20) — simple, can jump to any page, supports a page-number UI. But it's unstable if items are inserted/deleted (rows shift), and slow on huge datasets.
  • Cursor (?after=<id>) — stable under inserts/deletes, performant at scale, ideal for infinite scroll. But no arbitrary page jumps.

Choose offset for classic numbered-page tables, cursor for feeds/infinite scroll.

Infinite scroll vs paged

Infinite scroll accumulates items ([...prev, ...next]) and tracks hasMore; paged replaces items per page. Infinite scroll also needs scroll-restoration and often virtualization.

Let a library do it

React Query's useInfiniteQuery / paginated queries already model all of this — isLoading vs isFetching vs isFetchingNextPage, caching per page, keepPreviousData to avoid flicker on page change. Know what it's doing so you can explain or hand-roll it.

Other concerns

  • Cache previous pages so going back is instant.
  • keepPreviousData — don't flash a skeleton when paging; show the old page dimmed.
  • Sync page to the URL (?page=3) so it's shareable and survives refresh.
  • Reset to page 1 when filters/search change.
  • Error per page — let the user retry just the failed page.

The framing

"I track position (page or cursor), page size, items, and total/hasMore — plus a status enum, not a loading boolean, because I need to distinguish initial load, page change, and background refetch and render each differently: skeleton, dimmed-old-page, subtle indicator. Offset pagination for numbered tables, cursor for feeds. I'd reach for React Query's paginated/infinite queries since they model exactly this — keepPreviousData, per-page cache — and I'd sync the page to the URL and reset to page 1 on filter changes."

Follow-up questions

  • Why use a status enum instead of an isLoading boolean?
  • Offset vs cursor pagination — when does each fit?
  • How do you avoid a loading flicker when changing pages?
  • How do you keep pagination state in sync with the URL?

Common mistakes

  • One isLoading boolean for initial load, page change, and refetch.
  • Flashing a full skeleton on every page change instead of keepPreviousData.
  • Offset pagination on data that's frequently inserted/deleted — rows shift.
  • Not resetting to page 1 when filters change.
  • Not syncing page to the URL, so refresh loses position.

Performance considerations

  • Cache fetched pages to make back-navigation instant; keepPreviousData avoids re-render fl/flicker. For infinite scroll, virtualize the accumulated list so the DOM doesn't grow unbounded.

Edge cases

  • Items inserted/deleted between page loads (offset instability).
  • Last page partially full; empty result set.
  • Error on page N — retry just that page without losing others.
  • User changes filters mid-pagination.

Real-world examples

  • React Query useInfiniteQuery powering an infinite feed with per-page cache.
  • A data table with numbered offset pagination and page synced to the URL query string.

Senior engineer discussion

Seniors model loading as a status enum, distinguish the three loading kinds and render each appropriately, choose offset vs cursor deliberately, sync state to the URL, and lean on a query library while being able to explain its internals.

Related questions