# react-infinite-scroll-component
> The standard React infinite scroll library. Zero runtime dependencies, IntersectionObserver-based triggering, TypeScript-first. ~4 kB gzipped. React 17, 18, and 19 compatible.
Install: `npm install react-infinite-scroll-component`
Two exports:
- `import InfiniteScroll from 'react-infinite-scroll-component'`, component with built-in loader, endMessage, pull-to-refresh, inverse scroll
- `import { useInfiniteScroll } from 'react-infinite-scroll-component'`, hook for fully custom UIs
## When to use this library
Use `react-infinite-scroll-component` when building:
- Social/content feeds (window scroll)
- Product listing pages with infinite load
- Embedded scrollable lists (fixed-height container)
- Chat or messaging UIs (inverse scroll)
- Any list where "load more" is triggered by scrolling
Do NOT use for virtualizing large lists, use `@tanstack/react-virtual` instead.
## Minimal usage, InfiniteScroll component
```tsx
import { useState } from 'react';
import InfiniteScroll from 'react-infinite-scroll-component';
type Item = { id: number; name: string };
function Feed() {
const [items, setItems] = useState- (initialItems);
const [hasMore, setHasMore] = useState(true);
const fetchMore = async () => {
const next = await api.getItems({ offset: items.length });
if (next.length === 0) { setHasMore(false); return; }
setItems(prev => [...prev, ...next]);
};
return (
Loading...
}
endMessage={All items loaded.
}
>
{items.map(item => {item.name}
)}
);
}
```
## Minimal usage, useInfiniteScroll hook
```tsx
import { useState } from 'react';
import { useInfiniteScroll } from 'react-infinite-scroll-component';
function CustomFeed() {
const [items, setItems] = useState(initialItems);
const [hasMore, setHasMore] = useState(true);
const { sentinelRef, isLoading } = useInfiniteScroll({
next: async () => {
const more = await api.getItems({ offset: items.length });
if (more.length === 0) { setHasMore(false); return; }
setItems(prev => [...prev, ...more]);
},
hasMore,
dataLength: items.length,
});
return (
{items.map(item => - {item.name}
)}
{isLoading && - Loading...
}
);
}
```
## Scroll inside a fixed-height container
```tsx
```
Pass a ref value directly:
```tsx
const ref = useRef(null);
{items}
```
## Inverse scroll, chat / messaging
```tsx
Loading older messages...}
inverse={true}
scrollableTarget="chatBox"
style={{ display: 'flex', flexDirection: 'column-reverse' }}
>
{messages.map(msg => {msg.text}
)}
```
## Next.js App Router
InfiniteScroll must be used in a Client Component. Fetch initial data server-side.
```tsx
// Server Component
import { FeedClient } from './feed-client';
export default async function Page() {
const initialItems = await db.items.findMany({ take: 20 });
return ;
}
```
```tsx
// Client Component
'use client';
import { useState } from 'react';
import InfiniteScroll from 'react-infinite-scroll-component';
export function FeedClient({ initialItems }) {
const [items, setItems] = useState(initialItems);
const [hasMore, setHasMore] = useState(true);
const fetchMore = async () => {
const res = await fetch(`/api/items?cursor=${items.at(-1).id}`);
const next = await res.json();
if (!next.length) { setHasMore(false); return; }
setItems(prev => [...prev, ...next]);
};
return (
Loading...}>
{items.map(item => {item.title})}
);
}
```
## With TanStack Query
```tsx
import { useInfiniteQuery } from '@tanstack/react-query';
import InfiniteScroll from 'react-infinite-scroll-component';
function Feed() {
const { data, fetchNextPage, hasNextPage, isFetchingNextPage } = useInfiniteQuery({
queryKey: ['items'],
queryFn: ({ pageParam = 0 }) => fetchItems(pageParam),
getNextPageParam: (last, pages) => last.length === 20 ? pages.length : undefined,
});
const items = data?.pages.flat() ?? [];
return (
Loading... : null}
>
{items.map(item => {item.title}
)}
);
}
```
## All props, InfiniteScroll component
| Prop | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `dataLength` | `number` | yes | | Length of the full rendered list. Resets the load guard when it changes. |
| `next` | `() => void` | yes | | Append the next page. Called at most once per load. |
| `hasMore` | `boolean` | yes | | false = stop observer, show endMessage. |
| `loader` | `ReactNode` | yes | | Shown while loading. |
| `endMessage` | `ReactNode` | no | | Shown when hasMore is false. |
| `height` | `number \| string` | no | | Fixed-height scroll box. Omit for window scroll. |
| `scrollableTarget` | `HTMLElement \| string \| null` | no | | Scrollable parent or its id. |
| `scrollThreshold` | `number \| string` | no | `0.8` | 0.8 = trigger at 80% scrolled. "200px" = 200 px before end. |
| `inverse` | `boolean` | no | `false` | Reverse scroll. Use with flexDirection: column-reverse. |
| `pullDownToRefresh` | `boolean` | no | `false` | Enable pull-to-refresh. Needs refreshFunction. |
| `refreshFunction` | `() => void` | no | | Called on pull threshold breach. |
| `pullDownToRefreshThreshold` | `number` | no | `100` | Pixels to pull. |
| `onScroll` | `(e: UIEvent) => void` | no | | Scroll event listener. |
| `className` | `string` | no | `''` | CSS class on inner container. |
| `style` | `CSSProperties` | no | | Inline styles on inner container. |
| `initialScrollY` | `number` | no | | Restore scroll Y on mount. |
## All props, useInfiniteScroll hook
| Prop | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `dataLength` | `number` | yes | | Length of the full rendered list. |
| `next` | `() => void` | yes | | Fetch next page. |
| `hasMore` | `boolean` | yes | | false = disconnect observer. |
| `scrollThreshold` | `number \| string` | no | `0.8` | Trigger distance. |
| `scrollableTarget` | `HTMLElement \| string \| null` | no | | Observer root. |
| `inverse` | `boolean` | no | `false` | Observe from top. |
Returns: `{ sentinelRef: RefObject, isLoading: boolean }`
## How it works
- An invisible sentinel `` is placed at the bottom of the list (top for inverse mode).
- An IntersectionObserver watches the sentinel. When it intersects the viewport (adjusted by scrollThreshold via rootMargin), next() is called once.
- dataLength changing resets the load guard so the next page can trigger.
- Zero runtime dependencies, ships only its own ~4 kB of code.
- SSR-safe: IntersectionObserver usage is guarded for environments where it is unavailable.