Discuss how to manage state for pagination and loading indicators.
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.
Pagination state is more than "current page" — and the loading indicator isn't one boolean.
The state you actually need
{
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
- Initial load — no data yet → skeleton screen for the whole list.
- Page change — switching pages → spinner on the list, or disable pagination controls, often keep the old page visible until the new one arrives.
- 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.