Infinite Scroll & Pagination
Pagination controls how a large collection is divided into manageable pages. Infinite scroll removes explicit page controls and loads more content as the user approaches the end. The implementation quality depends heavily on the pagination model behind it.
Offset Versus Cursor
Offset pagination asks for limit=20&offset=40. It is simple, but it can duplicate or skip items when new rows are inserted before the current offset. Cursor pagination asks for items after a stable value such as the last item ID or creation timestamp.
type Page<T> = {
items: T[];
nextCursor: string | null;
};
type FeedItem = {
id: string;
createdAt: string;
title: string;
};
export async function fetchFeedPage(cursor: string | null): Promise<Page<FeedItem>> {
const params = new URLSearchParams({ limit: "20" });
if (cursor) params.set("after", cursor);
const response = await fetch(`/api/feed?${params}`);
if (!response.ok) throw new Error("Failed to load feed");
return response.json() as Promise<Page<FeedItem>>;
}
The server returns nextCursor from the last item in the page. When it returns null, there is no next page.
Sentinel Fetching
Place a sentinel element after the list. When it intersects, fetch the next page unless a request is already in flight or the list has ended.
export function observeInfiniteScroll(
sentinel: Element,
loadNextPage: () => void,
) {
const observer = new IntersectionObserver(([entry]) => {
if (entry?.isIntersecting) loadNextPage();
}, { rootMargin: "400px" });
observer.observe(sentinel);
return () => observer.disconnect();
}
Use skeleton rows during the next-page fetch so layout remains stable. Spinners communicate waiting, but skeletons preserve the rhythm of the list and reduce perceived jank.
TIP
Prefer cursor pagination for any list that can be updated in real time. Offset pagination is fine for static, rarely-changed datasets.
Further Learning
Search these terms to go deeper:
- “cursor pagination stable ordering” — avoiding duplicate and skipped items
- “IntersectionObserver infinite scroll sentinel” — loading pages near the viewport
- “infinite scroll accessibility” — preserving navigation and announcing new content
- “pagination skeleton loading states” — perceived performance for incremental lists