Skip to main content

Overview

The Infinite Scroll component provides a powerful solution for loading and displaying large datasets with automatic pagination. It works seamlessly across both Web and React Native platforms, with platform-specific optimizations and optional virtualization for handling massive datasets.

Installation

npx shadcn@latest add https://rigidui.com/r/infinite-scroll.json

Usage

import { InfiniteScroll } from "@/components/infinite-scroll";
import { useState, useCallback } from "react";

export default function MyComponent() {
  const [items, setItems] = useState([]);
  const [hasNext, setHasNext] = useState(true);
  const [loading, setLoading] = useState(false);

  const loadMore = useCallback(async () => {
    setLoading(true);
    try {
      const response = await fetch(`/api/items?page=${Math.floor(items.length / 10) + 1}`);
      const data = await response.json();
      setItems(prev => [...prev, ...data.items]);
      setHasNext(data.hasMore);
    } catch (error) {
      console.error('Failed to load items:', error);
    } finally {
      setLoading(false);
    }
  }, [items.length]);

  return (
    <InfiniteScroll
      items={items}
      hasNextPage={hasNext}
      isLoading={loading}
      onLoadMore={loadMore}
      threshold={200}
      initialLoad={true}
      renderItem={(item, index) => (
        <div key={item.id} className="p-4 border rounded-lg">
          <h3 className="font-semibold">{item.title}</h3>
          <p className="text-muted-foreground">{item.description}</p>
        </div>
      )}
      loader={() => (
        <div className="flex justify-center py-4">
          <div className="animate-spin rounded-full h-6 w-6 border-b-2 border-primary" />
        </div>
      )}
      endMessage={
        <div className="text-center py-4 text-muted-foreground">
          <p>You've reached the end! 🎉</p>
        </div>
      }
    />
  );
}

Features

  • Automatic Loading: Automatically loads more content when user scrolls near the bottom with smart load triggering across platforms
  • Cross-Platform Support: Available for both Web and React Native with platform-specific optimizations using Intersection Observer for Web and FlatList for React Native
  • Virtual Scrolling: Optional virtualization with TanStack Virtual for Web to handle massive datasets with thousands of items while maintaining smooth performance
  • High Performance: Uses efficient rendering strategies - Intersection Observer for Web and FlatList optimizations for React Native for better performance and battery life
  • Pull to Refresh: Built-in pull-to-refresh support for React Native with customizable refresh handlers and loading states
  • Flexible Configuration: Customizable loading triggers, custom loaders, error states, and support for reverse loading with dual rendering modes

When to Use Virtualization

Virtualization is recommended when dealing with large datasets to maintain optimal performance.

Use Regular Mode When:

  • Working with less than 1000 items
  • Items have varying heights that are hard to estimate
  • You need complex CSS layouts or animations

Use Virtualized Mode When:

  • Displaying 1000+ items
  • Items have consistent or predictable heights
  • Performance is critical for your use case
  • Working with data tables or feeds with many entries

API Reference

InfiniteScroll (Web)

items
T[]
required
Array of items to display in the infinite scroll list.
hasNextPage
boolean
required
Whether there are more items to load. When false, the end message will be shown.
isLoading
boolean
required
Whether the component is currently loading more items.
onLoadMore
() => void | Promise<void>
required
Function called when more items should be loaded. Can be synchronous or asynchronous.
renderItem
(item: T, index: number) => React.ReactNode
required
Function to render each item. Receives the item data and its index.
threshold
number
default:"100"
Distance in pixels from the bottom to trigger loading. Lower values load sooner.
loader
React.ComponentType
default:"DefaultLoader"
Custom loading component to show while loading more items.
endMessage
React.ReactNode
default:"Default end message"
Message shown when all items are loaded and hasNextPage is false.
errorMessage
React.ReactNode
Error message to display when loading fails. Use this to show error states.
className
string
Additional CSS classes for the container element.
itemClassName
string
Additional CSS classes for each item wrapper element.
reverse
boolean
default:"false"
Whether to load items in reverse order. Useful for chat interfaces where new messages appear at the bottom.
initialLoad
boolean
default:"false"
Whether to automatically load initial data on mount. Set to true to trigger loading immediately.
scrollableTarget
string
ID of custom scroll container element. Use when the scroll container is not the immediate parent.
virtualized
boolean
default:"false"
Enable virtualization for better performance with large datasets. Requires height to be set.
estimateSize
() => number
default:"() => 50"
Function to estimate item size for virtualization. Return the approximate height in pixels.
height
number
default:"400"
Container height in pixels when virtualized. Required when virtualized is true.
overscan
number
default:"5"
Number of items to render outside the visible viewport for virtualization. Higher values reduce blank areas during fast scrolling.

InfiniteScroll (React Native)

items
T[]
required
Array of items to display in the infinite scroll list.
hasNextPage
boolean
required
Whether there are more items to load.
isLoading
boolean
required
Whether the component is currently loading more items.
onLoadMore
() => void | Promise<void>
required
Function called when more items should be loaded.
renderItem
(item: T, index: number) => React.ReactNode
required
Function to render each item.
threshold
number
default:"0.5"
Distance threshold to trigger loading (0-1, represents percentage of list). 0.5 means trigger when scrolled to 50% from bottom.
loader
React.ComponentType
default:"ActivityIndicator"
Custom loading component. Defaults to React Native’s ActivityIndicator.
endMessage
React.ReactNode
default:"Default end message"
Message shown when all items are loaded.
errorMessage
React.ReactNode
Error message to display when loading fails.
className
string
NativeWind classes for the container.
itemClassName
string
NativeWind classes for each item wrapper.
reverse
boolean
default:"false"
Whether to load items in reverse order (inverted FlatList).
initialLoad
boolean
default:"false"
Whether to automatically load initial data on mount.
estimateSize
number
default:"50"
Estimated item size for FlatList optimization (getItemLayout).
keyExtractor
(item: T, index: number) => string
default:"Uses index"
Function to extract unique key for each item. Required for optimal performance.

Types

InfiniteScrollProps

interface InfiniteScrollProps<T> {
  items: T[];
  hasNextPage: boolean;
  isLoading: boolean;
  onLoadMore: () => void | Promise<void>;
  threshold?: number;
  loader?: React.ComponentType;
  endMessage?: React.ReactNode;
  errorMessage?: React.ReactNode;
  renderItem: (item: T, index?: number) => React.ReactNode;
  className?: string;
  itemClassName?: string;
  reverse?: boolean;
  initialLoad?: boolean;
  scrollableTarget?: string;
  virtualized?: boolean;
  estimateSize?: () => number;
  height?: number;
  overscan?: number;
}

Advanced Usage

Error Handling

import { InfiniteScroll } from "@/components/infinite-scroll";
import { useState, useCallback } from "react";
import { AlertCircle } from "lucide-react";

export default function MyComponent() {
  const [items, setItems] = useState([]);
  const [hasNext, setHasNext] = useState(true);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(null);

  const loadMore = useCallback(async () => {
    setLoading(true);
    setError(null);
    try {
      const response = await fetch('/api/items');
      if (!response.ok) throw new Error('Failed to load');
      const data = await response.json();
      setItems(prev => [...prev, ...data.items]);
      setHasNext(data.hasMore);
    } catch (err) {
      setError(err.message);
    } finally {
      setLoading(false);
    }
  }, []);

  return (
    <InfiniteScroll
      items={items}
      hasNextPage={hasNext}
      isLoading={loading}
      onLoadMore={loadMore}
      renderItem={(item) => <div>{item.title}</div>}
      errorMessage={
        error && (
          <div className="flex items-center justify-center gap-2 py-4 text-destructive">
            <AlertCircle className="h-4 w-4" />
            <span>{error}</span>
            <button onClick={loadMore} className="underline">Retry</button>
          </div>
        )
      }
    />
  );
}

Custom Scroll Container

import { InfiniteScroll } from "@/components/infinite-scroll";

export default function MyComponent() {
  return (
    <div id="custom-scrollable" className="h-screen overflow-auto">
      <div className="p-8">
        <h1>My Page</h1>
        <InfiniteScroll
          items={items}
          hasNextPage={hasNext}
          isLoading={loading}
          onLoadMore={loadMore}
          scrollableTarget="custom-scrollable"
          renderItem={(item) => <div>{item.title}</div>}
        />
      </div>
    </div>
  );
}
When using virtualization, ensure that height is set appropriately and items have predictable heights. Variable height items may cause layout shifting.
For optimal performance on React Native, always provide a keyExtractor function that returns a unique, stable identifier for each item.

Performance Tips

  1. Use virtualization for lists with 1000+ items
  2. Memoize renderItem function to prevent unnecessary re-renders
  3. Set appropriate threshold values (lower for slower networks)
  4. Implement proper error handling with retry mechanisms
  5. Use keyExtractor on React Native for better FlatList performance
  6. Estimate item sizes accurately for smoother virtualized scrolling

Build docs developers (and LLMs) love