ankeetmaini/react-infinite-scroll-component(新しいタブで開きます)
主要言語: TypeScript
An awesome Infinite Scroll component in react.
- star 数
- 3,090
- watcher 数
- 18
- fork 数
- 335
- issue 数
- 138
- 最終更新
- 2026/06/07
主要言語: TypeScript
An awesome Infinite Scroll component in react.
Infinite scroll for React. Zero runtime dependencies, IntersectionObserver-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.
npm install react-infinite-scroll-component # or yarn add react-infinite-scroll-component # or pnpm add react-infinite-scroll-component
| API | When to use |
|---|---|
InfiniteScroll component |
Most cases, handles loader, endMessage, pull-to-refresh, inverse scroll UI |
useInfiniteScroll hook |
Custom UI, you own the markup, the hook manages the observer |
import { useState } from 'react'; import InfiniteScroll from 'react-infinite-scroll-component'; type Item = { id: number; name: string }; function Feed() { const [items, setItems] = useState<Item[]>(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 ( <InfiniteScroll dataLength={items.length} next={fetchMore} hasMore={hasMore} loader={<p>Loading...</p>} endMessage={<p style={{ textAlign: 'center' }}>All items loaded.</p>} > {items.map((item) => ( <div key={item.id}>{item.name}</div> ))} </InfiniteScroll> ); }
<div id="scrollableDiv" style={{ height: 400, overflow: 'auto' }}> <InfiniteScroll dataLength={items.length} next={fetchMore} hasMore={hasMore} loader={<p>Loading...</p>} scrollableTarget="scrollableDiv" > {items.map((item) => ( <div key={item.id}>{item.name}</div> ))} </InfiniteScroll> </div>
Pass a ref value directly instead of a string id:
const containerRef = useRef<HTMLDivElement>(null); <div ref={containerRef} style={{ height: 400, overflow: 'auto' }}> <InfiniteScroll dataLength={items.length} next={fetchMore} hasMore={hasMore} loader={<p>Loading...</p>} scrollableTarget={containerRef.current} > {items.map((item) => ( <div key={item.id}>{item.name}</div> ))} </InfiniteScroll> </div>;
<div id="chatBox" style={{ height: 500, overflow: 'auto', display: 'flex', flexDirection: 'column-reverse', }} > <InfiniteScroll dataLength={messages.length} next={loadOlderMessages} hasMore={hasMore} loader={<p>Loading older messages...</p>} inverse={true} scrollableTarget="chatBox" style={{ display: 'flex', flexDirection: 'column-reverse' }} > {messages.map((msg) => ( <div key={msg.id}>{msg.text}</div> ))} </InfiniteScroll> </div>
<InfiniteScroll dataLength={items.length} next={fetchMore} hasMore={hasMore} loader={<p>Loading...</p>} pullDownToRefresh pullDownToRefreshThreshold={50} refreshFunction={refreshList} pullDownToRefreshContent={ <h3 style={{ textAlign: 'center' }}>↓ Pull down to refresh</h3> } releaseToRefreshContent={ <h3 style={{ textAlign: 'center' }}>↑ Release to refresh</h3> } > {items.map((item) => ( <div key={item.id}>{item.name}</div> ))} </InfiniteScroll>
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.
import { useState } from 'react'; import { useInfiniteScroll } from 'react-infinite-scroll-component'; type Item = { id: number; name: string }; function CustomFeed() { const [items, setItems] = useState<Item[]>(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 ( <ul> {items.map((item) => ( <li key={item.id}>{item.name}</li> ))} <li ref={sentinelRef} aria-hidden="true" /> {isLoading && <li>Loading...</li>} {!hasMore && <li>All items loaded.</li>} </ul> ); }
InfiniteScroll is a client component. Fetch initial data in a Server Component, pass it down.
// 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 <FeedClient initialItems={initialItems} />; }
// 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 ( <InfiniteScroll dataLength={items.length} next={fetchMore} hasMore={hasMore} loader={<p>Loading...</p>} endMessage={<p>You have seen everything.</p>} > {items.map((item) => ( <article key={item.id}>{item.title}</article> ))} </InfiniteScroll> ); }
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 ( <InfiniteScroll dataLength={posts.length} next={fetchNextPage} hasMore={!!hasNextPage} loader={isFetchingNextPage ? <p>Loading...</p> : null} endMessage={<p>All posts loaded.</p>} > {posts.map((post) => ( <article key={post.id}>{post.title}</article> ))} </InfiniteScroll> ); }
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 ( <InfiniteScroll dataLength={posts.length} next={() => setSize(size + 1)} hasMore={hasMore} loader={<p>Loading...</p>} > {posts.map((post) => ( <div key={post.id}>{post.title}</div> ))} </InfiniteScroll> ); }
| 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 |
| 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. |
Pass role and a label so screen readers can announce the container and its item count correctly:
<InfiniteScroll role="list" aria-label="Search results" dataLength={items.length} next={fetchMore} hasMore={hasMore} loader={<p>Loading...</p>} > {items.map((item) => ( <div role="listitem" key={item.id}> {item.name} </div> ))} </InfiniteScroll>
Or reference an existing heading via aria-labelledby:
<h2 id="results-heading">Search results</h2> <InfiniteScroll role="list" aria-labelledby="results-heading" dataLength={items.length} next={fetchMore} hasMore={hasMore} loader={<p>Loading...</p>} >
| 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 }.
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.throttle-debounce removed.scrollableTarget accepts HTMLElement, pass a ref value directly, not just a string id.scrollableTarget
Thanks goes to these wonderful people (emoji key):
This project follows the all-contributors specification. Contributions of any kind are welcome!