Infinite List Sentinel
Sentinel-based pagination with the stall fixed — the bug where a short page leaves the sentinel on screen and loading stops.
DOM + CSS0 deps1 file · 5 KB TSX
Props
"use client";
import { useEffect, useRef, useState } from "react";
export function InfiniteListSentinel({
pageSize = 12,
total = 60,
latency = 500,
prefetchPx = 200,
load,
}: {
pageSize?: number;
total?: number;
latency?: number;
/** How far below the fold to start loading. */
prefetchPx?: number;
/** Your real fetch. Omitted, a local stand-in is used. */
load?: (offset: number, limit: number) => Promise<string[]>;
// ---------------------------------------------------------------
// Full source available with a viberdy Pro subscription.
// https://viberdy.dev/pricing
// ---------------------------------------------------------------
}Details
About this pattern
Sentinel pagination has two failure modes that are exact opposites, and shipping a fix for one usually introduces the other. The obvious one is the double fetch: two intersection callbacks can land in the same tick and both read the same stale loading flag out of the closure, so the guard has to be a ref that updates synchronously rather than a piece of state. The subtle one — and the one almost everybody ships — is the stall. IntersectionObserver reports threshold CROSSINGS, not a continuing condition, so if a page of rows happens to be short enough that the sentinel is still on screen once they render, no further callback ever arrives and loading stops dead halfway down a list that has plenty more. The fix is to ask the question again after every load: attach a fresh short-lived observer and, if the sentinel is still intersecting, fetch again. Two more things separate this from the usual version. There is a real end condition, so the list stops requesting empty pages forever. And there is always a real Load more button alongside the sentinel, because infinite scroll with no manual trigger is a trap — there is no keyboard gesture for “scroll until more appears”, and anything below the list becomes permanently unreachable. New rows are announced through a polite live region, since appending silently just makes the page longer with no indication that anything happened.
Curator’s note
The stall is the interesting bug: IntersectionObserver fires on crossings, so a short page that leaves the sentinel visible never fires again.

