# 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 ( ); } ``` ## Scroll inside a fixed-height container ```tsx
Loading...

} scrollableTarget="scrollableDiv" > {items.map(item =>
{item.name}
)}
``` 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.