# react-infinite-scroll-component [](https://www.npmjs.com/package/react-infinite-scroll-component) [](https://www.npmjs.com/package/react-infinite-scroll-component) [](https://bundlephobia.com/package/react-infinite-scroll-component)
[](#contributors-)
Infinite scroll for React. Zero runtime dependencies, [IntersectionObserver](https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API)-based, TypeScript-first. ~4 kB gzipped.
Works with window scroll, fixed-height containers, and custom scrollable parents. Pull-to-refresh and inverse (chat) scroll included. React 17, 18, and 19 compatible.
## Install
```bash
npm install react-infinite-scroll-component
# or
yarn add react-infinite-scroll-component
# or
pnpm add react-infinite-scroll-component
```
## Two APIs
| API | When to use |
| ------------------------------------------------------- | -------------------------------------------------------------------------- |
| [`InfiniteScroll`](#infinitescroll-component) component | Most cases, handles loader, endMessage, pull-to-refresh, inverse scroll UI |
| [`useInfiniteScroll`](#useinfinitescroll-hook) hook | Custom UI, you own the markup, the hook manages the observer |
---
## `InfiniteScroll` component
### Basic usage (TypeScript)
```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}
))}
);
}
```
### Scroll inside a fixed-height container
```tsx
```
Pass a `ref` value directly instead of a string id:
```tsx
const containerRef = useRef(null);
Loading...}
scrollableTarget={containerRef.current}
>
{items.map((item) => (
{item.name}
))}
;
```
### Inverse scroll (chat / messaging UIs)
```tsx
Loading older messages...}
inverse={true}
scrollableTarget="chatBox"
style={{ display: 'flex', flexDirection: 'column-reverse' }}
>
{messages.map((msg) => (
{msg.text}
))}
```
### Pull-to-refresh
```tsx
Loading...}
pullDownToRefresh
pullDownToRefreshThreshold={50}
refreshFunction={refreshList}
pullDownToRefreshContent={
↓ Pull down to refresh
}
releaseToRefreshContent={
↑ Release to refresh
}
>
{items.map((item) => (
{item.name}
))}
```
---
## `useInfiniteScroll` hook
For when you need full control over your markup. Place the `sentinelRef` div at the end of your list, the hook fires `next()` when it enters the viewport.
```tsx
import { useState } from 'react';
import { useInfiniteScroll } from 'react-infinite-scroll-component';
type Item = { id: number; name: string };
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...
}
{!hasMore && - All items loaded.
}
);
}
```
---
## Framework recipes
### Next.js App Router
InfiniteScroll is a client component. Fetch initial data in a Server Component, pass it down.
```tsx
// app/feed/page.tsx, Server Component
import { FeedClient } from './feed-client';
import { db } from '@/lib/db';
export default async function FeedPage() {
const initialItems = await db.items.findMany({
take: 20,
orderBy: { id: 'desc' },
});
return ;
}
```
```tsx
// app/feed/feed-client.tsx, Client Component
'use client';
import { useState } from 'react';
import InfiniteScroll from 'react-infinite-scroll-component';
type Item = { id: string; title: string };
export function FeedClient({ initialItems }: { initialItems: Item[] }) {
const [items, setItems] = useState(initialItems);
const [hasMore, setHasMore] = useState(true);
const fetchMore = async () => {
const res = await fetch(`/api/items?cursor=${items[items.length - 1].id}`);
const next: Item[] = await res.json();
if (next.length === 0) {
setHasMore(false);
return;
}
setItems((prev) => [...prev, ...next]);
};
return (
Loading...}
endMessage={You have seen everything.
}
>
{items.map((item) => (
{item.title}
))}
);
}
```
### With TanStack Query
```tsx
import { useInfiniteQuery } from '@tanstack/react-query';
import InfiniteScroll from 'react-infinite-scroll-component';
function PostFeed() {
const { data, fetchNextPage, hasNextPage, isFetchingNextPage } =
useInfiniteQuery({
queryKey: ['posts'],
queryFn: ({ pageParam = 0 }) => fetchPosts(pageParam),
getNextPageParam: (lastPage, pages) =>
lastPage.length === 20 ? pages.length : undefined,
});
const posts = data?.pages.flat() ?? [];
return (
Loading... : null}
endMessage={All posts loaded.
}
>
{posts.map((post) => (
{post.title}
))}
);
}
```
### With SWR
```tsx
import useSWRInfinite from 'swr/infinite';
import InfiniteScroll from 'react-infinite-scroll-component';
const PAGE_SIZE = 20;
function PostList() {
const { data, size, setSize } = useSWRInfinite(
(index) => `/api/posts?page=${index}&limit=${PAGE_SIZE}`,
fetcher
);
const posts = data ? data.flat() : [];
const hasMore = data ? data[data.length - 1].length === PAGE_SIZE : true;
return (
setSize(size + 1)}
hasMore={hasMore}
loader={Loading...
}
>
{posts.map((post) => (
{post.title}
))}
);
}
```
---
## Three scroll modes
| Mode | How to use | Use case |
| ---------------------------- | --------------------------------------- | ------------------------------------- |
| **Window scroll** | Omit `height` and `scrollableTarget` | Social feeds, blogs, product listings |
| **Fixed-height container** | Pass `height` prop | Embedded lists, sidebars |
| **Custom scrollable parent** | Pass `scrollableTarget` (element or id) | Existing overflow containers |
---
## Props, `InfiniteScroll`
| Prop | Type | Required | Default | Description |
| ---------------------------- | ------------------------------- | -------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `dataLength` | `number` | yes | - | Current count of rendered items. The component resets its load guard each time this value changes, which allows `next()` to fire again on the next scroll. |
| `next` | `() => void` | yes | - | Called once when the sentinel enters the viewport. Append new items to your list state inside this callback; do not replace the existing items. |
| `hasMore` | `boolean` | yes | - | When `false`, the observer is disconnected and `next()` will not be called again. Set it to `false` when your data source has no more pages. |
| `loader` | `ReactNode` | yes | - | Rendered below the list while the next page is loading. Displayed between the last item and the bottom sentinel. |
| `endMessage` | `ReactNode` | no | - | Rendered below the list when `hasMore` is `false`. Use it for an "all caught up" or "no more items" message. |
| `height` | `number \| string` | no | - | Creates a fixed-height scroll container wrapping the list. Accepts a pixel number or any CSS length string. Omit this prop to scroll the window instead. |
| `scrollableTarget` | `HTMLElement \| string \| null` | no | - | The scrollable ancestor that already provides overflow scrollbars. Pass the element's `id` string or a direct `HTMLElement` reference. Required when the scroll container is neither the window nor the `height` wrapper. |
| `scrollThreshold` | `number \| string` | no | `0.8` | How close to the bottom the user must scroll before `next()` is called. A fraction like `0.8` means 80% scrolled; a string like `"200px"` means within 200 px of the bottom edge. |
| `inverse` | `boolean` | no | `false` | Reverse scroll direction for chat or messaging UIs. The sentinel moves to the top of the list. Use together with `flexDirection: column-reverse` on the scroll container. |
| `pullDownToRefresh` | `boolean` | no | `false` | Enable pull-to-refresh gesture on touch and mouse. Requires `refreshFunction` to also be set. |
| `refreshFunction` | `() => void` | no | - | Called once when the user pulls down past `pullDownToRefreshThreshold` pixels and releases. Only active when `pullDownToRefresh` is `true`. |
| `pullDownToRefreshThreshold` | `number` | no | `100` | How many pixels the user must pull down before `refreshFunction` is triggered on release. |
| `pullDownToRefreshContent` | `ReactNode` | no | - | Content shown inside the pull-to-refresh area while the user is pulling but has not yet reached the threshold. |
| `releaseToRefreshContent` | `ReactNode` | no | - | Content shown inside the pull-to-refresh area once the threshold is passed and the user can release to refresh. |
| `onScroll` | `(e: UIEvent) => void` | no | - | Callback fired on every scroll event on the container. Receives the native `UIEvent`. Useful for syncing UI state with scroll position. |
| `className` | `string` | no | `''` | CSS class name applied to the inner scroll container div. |
| `style` | `CSSProperties` | no | - | Inline style object applied to the inner scroll container div. Merged with the component's default layout styles. |
| `role` | `AriaRole` | no | - | Semantic role for the scroll container. Use `"list"` for item lists, `"feed"` for activity streams. |
| `tabIndex` | `number` | no | - | Makes the scroll container focusable. Pass `0` to include it in the natural tab sequence. |
| `id` | `string` | no | - | DOM id for the container. Useful when other elements reference it via `aria-labelledby`. |
| `aria-*` | `AriaAttributes` | no | - | Any React `aria-*` prop (`aria-label`, `aria-labelledby`, `aria-describedby`, etc.) forwarded to the scroll container. |
| `hasChildren` | `boolean` | no | - | Set to `true` when `children` is a single element or a fragment rather than an array. Helps the component detect whether visible content exists to determine scroll state. |
| `initialScrollY` | `number` | no | - | Scrolls the window to this Y offset on mount. Useful for restoring a user's scroll position when navigating back to a page. |
## Accessibility
Pass `role` and a label so screen readers can announce the container and its item count correctly:
```tsx
Loading...}
>
{items.map((item) => (
{item.name}
))}
```
Or reference an existing heading via `aria-labelledby`:
```tsx
Search results
Loading...}
>
```
---
## Props, `useInfiniteScroll`
| Prop | Type | Required | Default | Description |
| ------------------ | ------------------------------- | -------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `dataLength` | `number` | yes | - | Current count of rendered items. The hook resets its load guard whenever this value changes, allowing `next()` to fire again on the next intersection. |
| `next` | `() => void` | yes | - | Called once when the sentinel enters the viewport. Append new items to your list state inside this callback; do not replace the existing items. |
| `hasMore` | `boolean` | yes | - | When `false`, the `IntersectionObserver` is disconnected and `next()` will not be called again. Set it to `false` when your data source has no more pages. |
| `scrollThreshold` | `number \| string` | no | `0.8` | How close to the edge the sentinel must be before `next()` fires. A fraction like `0.8` means 80% scrolled; a string like `"200px"` means within 200 px of the edge. |
| `scrollableTarget` | `HTMLElement \| string \| null` | no | - | The scrollable ancestor to use as the observer root. Pass a DOM `id` string or an `HTMLElement` reference. When omitted, the observer uses the browser viewport. |
| `inverse` | `boolean` | no | `false` | When `true`, the rootMargin is applied to the top edge instead of the bottom. Place the sentinel at the top of your list and use `flexDirection: column-reverse` for chat UIs. |
Returns `{ sentinelRef, isLoading }`.
---
## What's new in v7
- **IntersectionObserver-based triggering**, `next()` fires once when the sentinel enters the viewport, not on every scroll tick. No missed triggers, better performance.
- **`useInfiniteScroll` hook**, low-level hook for building fully custom UIs.
- **Zero runtime dependencies**, `throttle-debounce` removed.
- **`scrollableTarget` accepts `HTMLElement`**, pass a ref value directly, not just a string id.
- **Function component rewrite**, same public API, no migration needed.
- **React 17, 18, 19** compatible.
---
## live examples
- infinite scroll (never ending), window scroll
- [](https://codesandbox.io/s/yk7637p62z)
- infinite scroll till 500 elements, window scroll
- [](https://codesandbox.io/s/439v8rmqm0)
- infinite scroll in an element (height 400px)
- [](https://codesandbox.io/s/w3w89k7x8)
- infinite scroll with `scrollableTarget`
- [](https://codesandbox.io/s/r7rp40n0zm)
---
## Contributors β¨
Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)):
This project follows the [all-contributors](https://allcontributors.org) specification. Contributions of any kind are welcome!
## LICENSE
[MIT](LICENSE)