Beta

Infinite Scroll

Infinite scroll or lazy loading for lists data, modern alternative of pagination.

Installation

Usage

function CardSkelton() {
  return (
    <div className="flex flex-col space-y-6">
      <div className="flex items-center gap-2">
        <Skeleton className="size-8 rounded-full" />
        <Skeleton className="w-full h-6 rounded" />
      </div>
      <Skeleton className="w-full h-24 rounded" />
    </div>
  );
}
export function InfiniteScrollDemo() {
  const BASE_URL = 'https://jsonplaceholder.typicode.com/posts';
  const LIMIT = 6;

  const [posts, setPosts] = useState<Post[]>([]);
  const [page, setPage] = useState<number>(0);
  const [totalCount, setTotalCount] = useState<number | null>();
  const [isPending, setIsPending] = useState<boolean>(false);

  const fetchData = useCallback(async () => {
    if (isPending) return;

    setIsPending(true);
    const start = page * LIMIT;

    try {
      const response = await fetch(
        `${BASE_URL}?_start=${start}&_limit=${LIMIT}`,
      );
      const totalItems = response.headers.get('x-total-count');
      const data = await response.json();

      setTotalCount(Number(totalItems));
      setPosts((prevPosts) => [...prevPosts, ...data]);
      setPage((prevPage) => prevPage + 1);
    } catch (error) {
      console.error('Error fetching data:', error);
    } finally {
      setIsPending(false);
    }
  }, [page, isPending]);

  useEffect(() => {
    fetchData();
  }, []);
  return (
    <InfiniteScroll
      isPending={isPending}
      currentItemsLength={posts.length}
      allItemsCount={totalCount}
      loadMore={fetchData}
      className="grid grid-cols-[repeat(auto-fill,minmax(250px,1fr))] justify-center items-start gap-4 p-8"
    >
      {posts.map((post) => (
        <InfiniteScrollCell
          className="p-6 bg-card border border-border/50 rounded-xl shadow-2xs"
          key={post.id}
          amount={0.5}
          skelton={<CardSkelton />}
        >
          <div className="flex flex-col space-y-6 ">
            <div className="flex items-center gap-2">
              <div className="font-semibold text-sm text-muted-foreground">
                #{post.id}
              </div>
              <h2
                title={post.title}
                className="font-medium max-w-[15ch] capitalize line-clamp-1"
              >
                {post.title}
              </h2>
            </div>
            <p className="text-sm text-muted-foreground">{post.body}</p>
          </div>
        </InfiniteScrollCell>
      ))}
    </InfiniteScroll>
  );
}

Props

InfiniteScrollCell

PropTypeDefault
amount?
UseInViewOptions['amount']
-
skelton?
ReactNode
-

InfiniteScroll

PropTypeDefault
loadMore
() => void
-
allItemsCount
number
-
isPending
boolean
-
currentItemsLength
number
-

InfiniteScroll vs Virtualization Libraries

This component uses deferred rendering — items mount once when they become visible and stay in the DOM. This differs from virtualization libraries like react-virtualized or @tanstack/virtual which unmount items as they leave the viewport.

When to use each

Items CountRecommendationReason
< 50✅ InfiniteScrollSimpler, smooth animations
50-200⚠️ Either worksInfiniteScroll works, but monitor memory on mobile
200+✅ VirtualizationMemory becomes a concern
1000+✅ VirtualizationRequired for performance

Feature comparison

FeatureInfiniteScrollVirtualization
PhilosophyDefer mounting, keep renderedOnly render visible items
Best forProduct grids, card galleriesLong lists, large tables
Animations✅ Skeleton → content transitions❌ No mount/unmount animations
Memory usageGrows with scrollConstant (~10-20 items)
Variable heights✅ Automatic⚠️ Requires configuration
ComplexitySimpleMore configuration needed

[!TIP] Use InfiniteScroll for ecommerce product grids, card galleries, and any list under 100 items where you want smooth skeleton-to-content animations.