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
npx shadcn@latest add https://rigidui.com/r/infinite-scroll-rn.json
Usage
Web - Basic
Web - Virtualized
React Native
Reverse Loading (Chat)
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>
}
/>
);
}
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 / 100) + 1}`);
const data = await response.json();
setItems(prev => [...prev, ...data.items]);
setHasNext(data.hasMore);
} finally {
setLoading(false);
}
}, [items.length]);
return (
<InfiniteScroll
items={items}
hasNextPage={hasNext}
isLoading={loading}
onLoadMore={loadMore}
virtualized={true}
height={600}
estimateSize={() => 80}
overscan={5}
renderItem={(item, index) => (
<div className="p-4 border-b">
<h3 className="font-semibold">{item.title}</h3>
<p className="text-sm text-muted-foreground">{item.description}</p>
</div>
)}
/>
);
}
import { InfiniteScroll } from "@/components/infinite-scroll-rn";
import { useState, useCallback } from "react";
import { View, Text } from "react-native";
export default function MyScreen() {
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={0.5}
initialLoad={true}
renderItem={(item, index) => (
<View className="p-4 border-b border-gray-200">
<Text className="font-semibold text-lg">{item.title}</Text>
<Text className="text-gray-600">{item.description}</Text>
</View>
)}
keyExtractor={(item) => item.id}
/>
);
}
import { InfiniteScroll } from "@/components/infinite-scroll";
import { useState, useCallback } from "react";
export default function ChatMessages() {
const [messages, setMessages] = useState([]);
const [hasNext, setHasNext] = useState(true);
const [loading, setLoading] = useState(false);
const loadOlderMessages = useCallback(async () => {
setLoading(true);
try {
const oldestId = messages[0]?.id;
const response = await fetch(`/api/messages?before=${oldestId}`);
const data = await response.json();
setMessages(prev => [...data.messages, ...prev]);
setHasNext(data.hasMore);
} finally {
setLoading(false);
}
}, [messages]);
return (
<InfiniteScroll
items={messages}
hasNextPage={hasNext}
isLoading={loading}
onLoadMore={loadOlderMessages}
reverse={true}
renderItem={(message) => (
<div className="p-3 border-b">
<div className="font-semibold">{message.author}</div>
<div className="text-sm">{message.content}</div>
</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
Array of items to display in the infinite scroll list.
Whether there are more items to load. When false, the end message will be shown.
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.
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.
Error message to display when loading fails. Use this to show error states.
Additional CSS classes for the container element.
Additional CSS classes for each item wrapper element.
Whether to load items in reverse order. Useful for chat interfaces where new messages appear at the bottom.
Whether to automatically load initial data on mount. Set to true to trigger loading immediately.
ID of custom scroll container element. Use when the scroll container is not the immediate parent.
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.
Container height in pixels when virtualized. Required when virtualized is true.
Number of items to render outside the visible viewport for virtualization. Higher values reduce blank areas during fast scrolling.
Array of items to display in the infinite scroll list.
Whether there are more items to load.
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.
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.
Error message to display when loading fails.
NativeWind classes for the container.
NativeWind classes for each item wrapper.
Whether to load items in reverse order (inverted FlatList).
Whether to automatically load initial data on mount.
Estimated item size for FlatList optimization (getItemLayout).
Function to extract unique key for each item. Required for optimal performance.
Types
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>
)
}
/>
);
}
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.
- Use virtualization for lists with 1000+ items
- Memoize renderItem function to prevent unnecessary re-renders
- Set appropriate threshold values (lower for slower networks)
- Implement proper error handling with retry mechanisms
- Use keyExtractor on React Native for better FlatList performance
- Estimate item sizes accurately for smoother virtualized scrolling