react-infinite-scroll-component、react-infinite-scroller 和 react-window-infinite-loader 都是用于在 React 应用中实现无限滚动加载功能的库,但它们的实现方式和适用场景有所不同。react-infinite-scroll-component 提供简单的容器包装方案,适合快速集成基础无限滚动需求。react-infinite-scroller 是较早的解决方案,支持多种滚动容器类型。react-window-infinite-loader 基于 react-window 虚拟化技术,专为处理大型数据集优化,通过只渲染可见区域来提升性能。
在 React 应用中实现无限滚动加载是常见需求,但不同方案的性能表现和适用场景差异很大。react-infinite-scroll-component、react-infinite-scroller 和 react-window-infinite-loader 代表了三种不同的技术路线。让我们从实际工程角度深入对比。
react-infinite-scroll-component 采用传统渲染方式,所有加载的数据都会保留在 DOM 中。
// react-infinite-scroll-component: 基础用法
import InfiniteScroll from 'react-infinite-scroll-component';
function UserList() {
const [users, setUsers] = useState([]);
const fetchMoreData = () => {
// 加载更多数据
api.getUsers().then(newUsers => {
setUsers([...users, ...newUsers]);
});
};
return (
<InfiniteScroll
dataLength={users.length}
next={fetchMoreData}
hasMore={true}
loader={<h4>加载中...</h4>}
>
{users.map(user => (
<UserCard key={user.id} user={user} />
))}
</InfiniteScroll>
);
}
react-infinite-scroller 同样采用传统渲染方式,是较早的解决方案。
// react-infinite-scroller: 容器滚动模式
import InfiniteScroll from 'react-infinite-scroller';
function UserList() {
const [users, setUsers] = useState([]);
const handleInfiniteOnLoad = () => {
api.getUsers().then(newUsers => {
setUsers([...users, ...newUsers]);
});
};
return (
<div className="scroll-container" style={{ height: '500px', overflow: 'scroll' }}>
<InfiniteScroll
pageStart={0}
loadMore={handleInfiniteOnLoad}
hasMore={true}
loader={<div key="loader">加载中...</div>}
>
{users.map(user => (
<UserCard key={user.id} user={user} />
))}
</InfiniteScroll>
</div>
);
}
react-window-infinite-loader 采用虚拟化渲染技术,只渲染可见区域。
react-window 底层能力,性能优异FixedSizeList 或 VariableSizeList 使用// react-window-infinite-loader: 虚拟化渲染
import InfiniteLoader from 'react-window-infinite-loader';
import { FixedSizeList } from 'react-window';
function UserList() {
const [users, setUsers] = useState([]);
const isItemLoaded = index => index < users.length;
const loadMoreItems = (startIndex, stopIndex) => {
return api.getUsers(startIndex, stopIndex).then(newUsers => {
setUsers([...users, ...newUsers]);
});
};
const Row = ({ index, style }) => (
<div style={style}>
{isItemLoaded(index) ? (
<UserCard user={users[index]} />
) : (
<div>加载中...</div>
)}
</div>
);
return (
<InfiniteLoader
isItemLoaded={isItemLoaded}
itemCount={users.length + 100}
loadMoreItems={loadMoreItems}
>
{({ onItemsRendered, ref }) => (
<FixedSizeList
height={500}
itemCount={users.length}
itemSize={100}
onItemsRendered={onItemsRendered}
ref={ref}
>
{Row}
</FixedSizeList>
)}
</InfiniteLoader>
);
}
react-infinite-scroll-component 主要支持容器内滚动。
scrollableTarget 属性指定滚动容器// react-infinite-scroll-component: 指定滚动目标
<InfiniteScroll
dataLength={items.length}
next={fetchMore}
hasMore={true}
scrollableTarget="scrollableDiv"
loader={<h4>加载中...</h4>}
>
{items.map(item => <Item key={item.id} {...item} />)}
</InfiniteScroll>
// HTML 中需要对应 ID
div id="scrollableDiv" style={{ height: '500px', overflow: 'auto' }}
react-infinite-scroller 明确区分窗口滚动和容器滚动。
useWindow 属性控制是否监听窗口滚动// react-infinite-scroller: 窗口滚动模式
<InfiniteScroll
pageStart={0}
loadMore={loadMore}
hasMore={true}
useWindow={true}
loader={<div key="loader">加载中...</div>}
>
{items.map(item => <Item key={item.id} {...item} />)}
</InfiniteScroll>
// react-infinite-scroller: 容器滚动模式
div style={{ height: '500px', overflow: 'auto' }}
<InfiniteScroll
pageStart={0}
loadMore={loadMore}
hasMore={true}
useWindow={false}
loader={<div key="loader">加载中...</div>}
>
{items.map(item => <Item key={item.id} {...item} />)}
</InfiniteScroll>
/div>
react-window-infinite-loader 专注于容器内虚拟化列表。
react-window 的列表组件使用// react-window-infinite-loader: 固定高度容器
<div style={{ height: '600px', overflow: 'auto' }}>
<InfiniteLoader
isItemLoaded={isItemLoaded}
itemCount={itemCount}
loadMoreItems={loadMoreItems}
>
{({ onItemsRendered, ref }) => (
<FixedSizeList
height={600}
itemCount={itemCount}
itemSize={50}
onItemsRendered={onItemsRendered}
ref={ref}
>
{Row}
</FixedSizeList>
)}
</InfiniteLoader>
</div>
react-infinite-scroll-component 在数据量增长时性能下降明显。
// 性能问题示例:大量数据时渲染变慢
// 以下代码在数据超过 1000 条时会有明显延迟
<InfiniteScroll
dataLength={items.length} // 可能是 5000+
next={fetchMore}
hasMore={true}
>
{items.map(item => <Item key={item.id} {...item} />)}
{/* 所有 5000+ 个 Item 组件都存在于 DOM 中 */}
</InfiniteScroll>
react-infinite-scroller 性能特征与前者类似。
// 同样的性能限制
<InfiniteScroll
pageStart={0}
loadMore={loadMore}
hasMore={true}
>
{items.map(item => <Item key={item.id} {...item} />)}
{/* 数据量大时同样会累积 DOM 节点 */}
</InfiniteScroll>
react-window-infinite-loader 性能表现最优。
// 虚拟化优势:只渲染可见区域
<InfiniteLoader
itemCount={100000} // 可以处理 10 万条数据
isItemLoaded={isItemLoaded}
loadMoreItems={loadMoreItems}
>
{({ onItemsRendered, ref }) => (
<FixedSizeList
height={600}
itemCount={100000}
itemSize={50}
onItemsRendered={onItemsRendered}
ref={ref}
>
{Row} {/* 实际只渲染约 12-15 个 Row 组件 */}
</FixedSizeList>
)}
</InfiniteLoader>
react-infinite-scroll-component 提供内置 loader 支持。
loader 属性直接传入加载组件<InfiniteScroll
dataLength={items.length}
next={fetchMore}
hasMore={true}
loader={<h4 key="loader">加载中...</h4>}
endMessage={<p style={{ textAlign: 'center' }}>没有更多数据了</p>}
>
{items.map(item => <Item key={item.id} {...item} />)}
</InfiniteScroll>
react-infinite-scroller 同样提供 loader 支持。
loader 属性需要唯一 key<InfiniteScroll
pageStart={0}
loadMore={loadMore}
hasMore={true}
loader={<div key="loader" className="loader">加载中...</div>}
>
{items.map(item => <Item key={item.id} {...item} />)}
</InfiniteScroll>
react-window-infinite-loader 需要手动管理加载状态。
Row 组件中判断是否已加载const Row = ({ index, style }) => {
if (!isItemLoaded(index)) {
return (
<div style={style}>
<LoadingSkeleton />
</div>
);
}
return (
<div style={style}>
<UserCard user={users[index]} />
</div>
);
};
<InfiniteLoader
isItemLoaded={isItemLoaded}
itemCount={itemCount}
loadMoreItems={loadMoreItems}
>
{({ onItemsRendered, ref }) => (
<FixedSizeList
height={600}
itemCount={itemCount}
itemSize={50}
onItemsRendered={onItemsRendered}
ref={ref}
>
{Row}
</FixedSizeList>
)}
</InfiniteLoader>
react-infinite-scroll-component 依赖 React 状态更新触发重新渲染。
function UserList() {
const [items, setItems] = useState([]);
const [page, setPage] = useState(1);
const fetchMore = () => {
api.getItems(page).then(newItems => {
setItems([...items, ...newItems]);
setPage(page + 1);
});
};
// 刷新需要重置状态
const refresh = () => {
setItems([]);
setPage(1);
fetchMore();
};
return (
<InfiniteScroll
dataLength={items.length}
next={fetchMore}
hasMore={true}
loader={<h4>加载中...</h4>}
>
{items.map(item => <Item key={item.id} {...item} />)}
</InfiniteScroll>
);
}
react-infinite-scroller 数据更新方式类似。
pageStart 属性可追踪当前页码function UserList() {
const [items, setItems] = useState([]);
const [page, setPage] = useState(0);
const loadMore = () => {
api.getItems(page).then(newItems => {
setItems([...items, ...newItems]);
setPage(page + 1);
});
};
return (
<InfiniteScroll
pageStart={page}
loadMore={loadMore}
hasMore={true}
loader={<div key="loader">加载中...</div>}
>
{items.map(item => <Item key={item.id} {...item} />)}
</InfiniteScroll>
);
}
react-window-infinite-loader 提供更精细的数据控制。
isItemLoaded 函数精确判断每项加载状态function UserList() {
const [items, setItems] = useState({});
const isItemLoaded = index => items[index] !== undefined;
const loadMoreItems = (startIndex, stopIndex) => {
const indicesToLoad = [];
for (let i = startIndex; i <= stopIndex; i++) {
if (!items[i]) {
indicesToLoad.push(i);
}
}
return api.getItems(indicesToLoad).then(newItems => {
setItems(prev => ({ ...prev, ...newItems }));
});
};
const Row = ({ index, style }) => (
<div style={style}>
{isItemLoaded(index) ? (
<Item item={items[index]} />
) : (
<LoadingSkeleton />
)}
</div>
);
return (
<InfiniteLoader
isItemLoaded={isItemLoaded}
itemCount={10000}
loadMoreItems={loadMoreItems}
>
{({ onItemsRendered, ref }) => (
<FixedSizeList
height={600}
itemCount={10000}
itemSize={50}
onItemsRendered={onItemsRendered}
ref={ref}
>
{Row}
</FixedSizeList>
)}
</InfiniteLoader>
);
}
react-infinite-scroll-component// 社交媒体信息流适合传统方案
<InfiniteScroll
dataLength={posts.length}
next={loadMorePosts}
hasMore={hasMorePosts}
loader={<PostSkeleton />}
>
{posts.map(post => (
<PostCard
key={post.id}
post={post}
onLike={handleLike}
onComment={handleComment}
/>
))}
</InfiniteScroll>
react-window-infinite-loader// 大型表格必须使用虚拟化方案
<InfiniteLoader
isItemLoaded={isRowLoaded}
itemCount={totalRows}
loadMoreItems={loadMoreRows}
>
{({ onItemsRendered, ref }) => (
<FixedSizeList
height={600}
itemCount={totalRows}
itemSize={40}
onItemsRendered={onItemsRendered}
ref={ref}
>
{Row}
</FixedSizeList>
)}
</InfiniteLoader>
react-window-infinite-loader// 聊天记录需要虚拟化 + 双向加载
<InfiniteLoader
isItemLoaded={isMessageLoaded}
itemCount={totalMessages}
loadMoreItems={loadMoreMessages}
>
{({ onItemsRendered, ref }) => (
<FixedSizeList
height={500}
itemCount={totalMessages}
itemSize={60}
onItemsRendered={onItemsRendered}
ref={ref}
>
{MessageRow}
</FixedSizeList>
)}
</InfiniteLoader>
react-infinite-scroll-component 目前维护活跃。
react-infinite-scroller 维护活跃度较低。
react-window-infinite-loader 依托 react-window 生态。
react-window 由 React 团队成员维护| 特性 | react-infinite-scroll-component | react-infinite-scroller | react-window-infinite-loader |
|---|---|---|---|
| 渲染方式 | 传统全量渲染 | 传统全量渲染 | 虚拟化渲染 |
| 性能表现 | 500 条以内流畅 | 500 条以内流畅 | 10 万 + 条流畅 |
| 内存占用 | 随数据量增长 | 随数据量增长 | 基本恒定 |
| 学习成本 | 低 | 低 | 中高 |
| 容器支持 | 容器/窗口 | 容器/窗口 | 容器为主 |
| 加载状态 | 内置 loader | 内置 loader | 手动管理 |
| 维护状态 | 活跃 | 较低 | 活跃(依托 react-window) |
| 适用场景 | 内容流、评论列表 | 旧项目维护 | 大数据表格、聊天记录 |
选择 react-infinite-scroll-component 如果:
选择 react-infinite-scroller 如果:
选择 react-window-infinite-loader 如果:
无限滚动看似简单,但技术选型会影响长期维护成本。数据量是核心判断标准——500 条以下可以用简单方案,超过这个阈值就应该考虑虚拟化。同时要考虑团队技术栈和用户设备性能,移动端用户对性能更敏感。
如果项目刚开始且不确定数据规模,建议直接采用 react-window-infinite-loader,虽然学习成本稍高,但能避免后期性能问题导致的重构。
选择 react-infinite-scroll-component 如果你需要快速实现基础无限滚动功能,且数据量不大(几百条以内)。它 API 简单,学习成本低,适合内容流、评论列表等场景。但要注意它不采用虚拟化技术,大量数据时会有性能问题。
选择 react-window-infinite-loader 如果你需要处理成千上万条数据,或对性能有严格要求。它采用虚拟化技术,只渲染可见区域,内存占用低。适合数据表格、大型列表、聊天记录等场景,但学习曲线较陡峭。
选择 react-infinite-scroller 仅用于维护旧项目。该包已不再积极维护,官方文档显示其功能已被其他方案替代。新项目不建议使用,应优先考虑 react-infinite-scroll-component 或基于虚拟化的解决方案。
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 |
InfiniteScroll componentimport { 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>
useInfiniteScroll hookFor 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 |
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. |
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>}
>
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 }.
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!