Aur UI

Use Infinite Scroll

Use Infinite Scroll from Aur UI.

bash
pnpm add @aur-ui/ui
tsx
import { useInfiniteScroll } from "@aur-ui/ui/hooks/use-infinite-scroll";
hooks/use-infinite-scroll.ts
"use client";

import { useEffect, useRef } from "react";

/**
 * Bottom-of-list infinite scroll. The consumer renders the returned ref on a
 * sentinel `<div>` at the end of the list; an `IntersectionObserver` watches it
 * and calls `onLoadMore` once it scrolls into view — as long as there is more
 * to load and a fetch isn't already in flight.
 *
 * The observer re-subscribes when `hasMore` / `isFetchingMore` change, so after
 * a page finishes loading it re-arms and (if the sentinel is still visible, e.g.
 * a short page) fetches the next one until the list is exhausted. `onLoadMore`
 * is read through a ref so a changing callback identity doesn't churn the
 * observer. No-ops where `IntersectionObserver` is absent (SSR / jsdom).
 */
export function useInfiniteScroll({
  hasMore,
  isFetchingMore,
  onLoadMore,
  rootMargin = "200px",
}: {
  hasMore: boolean;
  isFetchingMore: boolean;
  onLoadMore: () => void;
  /** How far ahead of the viewport to prefetch. Defaults to 200px. */
  rootMargin?: string;
}) {
  const sentinelRef = useRef<HTMLDivElement | null>(null);
  const onLoadMoreRef = useRef(onLoadMore);
  onLoadMoreRef.current = onLoadMore;

  useEffect(() => {
    const el = sentinelRef.current;
    if (!el || !hasMore || isFetchingMore) return;
    if (typeof IntersectionObserver === "undefined") return;

    const observer = new IntersectionObserver(
      (entries) => {
        if (entries.some((entry) => entry.isIntersecting)) {
          onLoadMoreRef.current();
        }
      },
      // Prefetch slightly before the sentinel is fully in view.
      { rootMargin },
    );
    observer.observe(el);
    return () => observer.disconnect();
  }, [hasMore, isFetchingMore, rootMargin]);

  return sentinelRef;
}