Skip to content

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

Prominimal
Preview

Props

12
60
500
200

The source is part of Pro

$27/month or $179 once. See Pro

No runtime dependencies
"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.

Pairs well with

Pro