Lightweight infinite scroll with pagination (page/cursor), virtual list, grid layout and window scroll support for React
npm install react-virtual-infinite-listA powerful, flexible infinite scroll library for React with support for pagination, cursor-based loading, virtualization, and grid layouts.
- Page-based pagination - Traditional pagination with page numbers and total count
- Cursor-based pagination - Efficient cursor/token-based pagination
- Virtual list - Efficiently render 10,000+ items by only rendering visible items
- Window scroll - Use browser window scroll instead of fixed container
- Grid layout - Responsive grid with auto-fill columns
- Auto-load to index - Jump to any index with automatic data loading
- TypeScript - Full type safety with generics
``bash`
npm install react-virtual-infinite-list
`tsx
import { InfiniteList } from 'react-virtual-infinite-list'
function App() {
return (
const res = await fetch(/api/items?page=${page}&limit=${limit})`
return res.json()
}}
pagination={{ type: 'page', limit: 20 }}
getData={(res) => res.data}
getTotal={(res) => res.total}
>
{(item, index) => (
{index + 1}. {item.name}
)}
)
}
`tsx/api/items?page=${page}&limit=${limit}
const res = await fetch()`
return res.json()
}}
pagination={{ type: 'page', limit: 20 }}
getData={(res) => res.data}
getTotal={(res) => res.total}
keyExtractor={(item) => item.id}
loader={Loading...}
endMessage={No more items}
emptyMessage={No items found}
>
{(item, index) =>
`tsx/api/items?cursor=${cursor}&limit=${limit}
const url = cursor
? /api/items?limit=${limit}
: `
const res = await fetch(url)
return res.json()
}}
pagination={{ type: 'cursor', limit: 20 }}
getData={(res) => res.data}
getNextCursor={(res) => res.nextCursor}
keyExtractor={(item) => item.id}
>
{(item) =>
`tsx`
pagination={{ type: 'page', limit: 100 }}
getData={(res) => res.data}
getTotal={(res) => res.total}
virtualized={{
itemHeight: 80, // Fixed height per item (or function)
overscan: 5, // Extra items to render above/below viewport
containerHeight: 500, // Fixed container height
}}
>
{(item, index) =>
`tsx`
pagination={{ type: 'page', limit: 100 }}
getData={(res) => res.data}
getTotal={(res) => res.total}
virtualized={{
itemHeight: 80,
overscan: 10,
useWindowScroll: true, // Use window scroll instead of container
}}
>
{(item, index) =>
`tsx`
pagination={{ type: 'page', limit: 20 }}
getData={(res) => res.data}
getTotal={(res) => res.total}
layout="grid"
grid={{
columns: 'auto-fill', // or number like 3, 4
minItemWidth: 200, // min width for auto-fill
gap: 16, // gap between items
}}
>
{(item) =>
`tsx
import { useRef } from 'react'
import { InfiniteList, InfiniteListRef } from 'react-virtual-infinite-list'
function App() {
const listRef = useRef
const handleJumpTo = async (index: number) => {
// Auto-loads data if needed, then scrolls to index
await listRef.current?.scrollToIndexWithAutoLoad(index, 'center')
}
return (
<>
{(item) =>
>
)
}
`
`tsx
import { useInfiniteList } from 'react-virtual-infinite-list'
function CustomList() {
const {
items,
isLoading,
isLoadingMore,
error,
hasMore,
loadMore,
loadUntilCount,
reset,
refresh,
} = useInfiniteList({
fetcher: async ({ page, limit }) => {
const res = await fetch(/api/items?page=${page}&limit=${limit})
return res.json()
},
pagination: { type: 'page', limit: 20 },
getData: (res) => res.data,
getTotal: (res) => res.total,
})
if (isLoading) return
return (
API Reference
$3
| Prop | Type | Required | Description |
|------|------|----------|-------------|
|
fetcher | (params) => Promise | Yes | Function to fetch data |
| pagination | PagePagination \| CursorPagination | Yes | Pagination configuration |
| getData | (response: TData) => TItem[] | Yes | Extract items array from response |
| getTotal | (response: TData) => number | No | Get total count (page pagination) |
| getNextCursor | (response: TData) => string \| null | No | Get next cursor (cursor pagination) |
| hasMore | (response, items) => boolean | No | Custom hasMore logic |
| children | (item: TItem, index: number) => ReactNode | Yes | Render function for each item |
| keyExtractor | (item: TItem, index: number) => string \| number | No | Extract unique key |
| layout | 'list' \| 'grid' | No | Layout mode (default: 'list') |
| grid | GridOptions | No | Grid configuration |
| virtualized | VirtualListOptions | No | Virtual list configuration |
| loader | ReactNode | No | Loading indicator |
| endMessage | ReactNode | No | End of list message |
| errorMessage | ReactNode \| ((error, retry) => ReactNode) | No | Error display |
| emptyMessage | ReactNode | No | Empty state |
| threshold | number | No | Load more threshold in pixels (default: 200) |
| reverse | boolean | No | Reverse scroll direction |
| className | string | No | Container class |
| style | CSSProperties | No | Container style |
| containerClassName | string | No | Items container class |
| containerStyle | CSSProperties | No | Items container style |
| onLoadMore | () => void | No | Callback when loading more |
| onError | (error: Error) => void | No | Error callback |
| onSuccess | (data, items) => void | No | Success callback |
| enabled | boolean | No | Enable/disable fetching (default: true) |$3
`tsx
// Page-based pagination
type PagePagination = {
type: 'page'
initialPage?: number // default: 1
limit?: number // default: 20
}// Cursor-based pagination
type CursorPagination = {
type: 'cursor'
initialCursor?: string | null
limit?: number // default: 20
}
`$3
`tsx
type GridOptions = {
columns?: number | 'auto-fill' | 'auto-fit' // default: 'auto-fill'
minItemWidth?: number // default: 200
gap?: number | string // default: 16
rowGap?: number | string
columnGap?: number | string
}
`$3
`tsx
type VirtualListOptions = {
itemHeight: number | ((index: number) => number) // Required
overscan?: number // Extra items to render (default: 5)
containerHeight?: number // Fixed height (required if not useWindowScroll)
useWindowScroll?: boolean // Use window scroll (default: false)
}
`$3
`tsx
type InfiniteListRef = {
reset: () => void
refresh: () => Promise
scrollToIndex: (index: number, align?: 'start' | 'center' | 'end') => void
scrollToIndexWithAutoLoad: (index: number, align?: 'start' | 'center' | 'end') => Promise
getLoadedCount: () => number
}
`$3
`tsx
type UseInfiniteListReturn = {
items: TItem[]
isLoading: boolean
isLoadingMore: boolean
error: Error | null
hasMore: boolean
loadMore: () => Promise
loadUntilCount: (targetCount: number) => Promise
reset: () => void
refresh: () => Promise
}type LoadResult = {
itemsCount: number
hasMore: boolean
}
`Hooks
$3
Core hook for infinite list logic.
`tsx
const result = useInfiniteList({
fetcher,
pagination,
getData,
getTotal,
getNextCursor,
hasMore,
enabled,
onSuccess,
onError,
})
`$3
Hook for virtual scrolling (used internally).
`tsx
const { virtualItems, totalHeight, containerRef, scrollToIndex } = useVirtualList({
items,
itemHeight,
overscan,
onNearEnd,
nearEndThreshold,
useWindowScroll,
})
`$3
Hook for intersection observer (used internally for non-virtual infinite scroll).
`tsx
const { targetRef, isIntersecting } = useIntersectionObserver({
threshold,
root,
rootMargin,
enabled,
onIntersect,
})
``MIT