react-infinite-scroll-component vs react-window-infinite-loader vs react-infinite-scroller
React 无限滚动组件技术选型对比
react-infinite-scroll-componentreact-window-infinite-loaderreact-infinite-scroller类似的npm包:

React 无限滚动组件技术选型对比

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 虚拟化技术,专为处理大型数据集优化,通过只渲染可见区域来提升性能。

npm下载趋势

3 年

GitHub Stars 排名

统计详情

npm包名称
下载量
Stars
大小
Issues
发布时间
License
react-infinite-scroll-component944,3893,092217 kB1363 个月前MIT
react-window-infinite-loader537,82195223 kB08 个月前MIT
react-infinite-scroller03,29530.3 kB98-MIT

React 无限滚动方案:性能、架构与实战对比

在 React 应用中实现无限滚动加载是常见需求,但不同方案的性能表现和适用场景差异很大。react-infinite-scroll-component、react-infinite-scroller 和 react-window-infinite-loader 代表了三种不同的技术路线。让我们从实际工程角度深入对比。

📦 核心架构:传统渲染 vs 虚拟化渲染

react-infinite-scroll-component 采用传统渲染方式,所有加载的数据都会保留在 DOM 中。

  • 实现简单,直接包装子组件即可
  • 数据量大时会导致 DOM 节点过多,影响性能
  • 适合数据量可控的场景(通常 500 条以内)
// 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>
  );
}

🎯 滚动容器支持:窗口滚动 vs 自定义容器

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 在数据量增长时性能下降明显。

  • 所有数据都保留在 DOM 中
  • 1000 条以上数据可能出现滚动卡顿
  • 内存占用随数据量线性增长
// 性能问题示例:大量数据时渲染变慢
// 以下代码在数据超过 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 性能特征与前者类似。

  • 同样存在 DOM 节点累积问题
  • 没有内置的虚拟化优化
  • 适合小到中等数据量场景
// 同样的性能限制
<InfiniteScroll
  pageStart={0}
  loadMore={loadMore}
  hasMore={true}
>
  {items.map(item => <Item key={item.id} {...item} />)}
  {/* 数据量大时同样会累积 DOM 节点 */}
</InfiniteScroll>

react-window-infinite-loader 性能表现最优。

  • 只渲染可见区域(通常 10-20 个组件)
  • 可轻松处理 10 万 + 数据量
  • 内存占用基本恒定,不随数据量增长
// 虚拟化优势:只渲染可见区域
<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>

🔧 加载状态管理:内置 loader vs 自定义逻辑

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
  • 可自定义加载组件样式
  • 功能与前者的 loader 类似
<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>
  );
}

🌐 实际应用场景推荐

场景 1:社交媒体信息流

  • 数据量:中等(通常几百条)
  • 交互:点赞、评论等频繁操作
  • ✅ 推荐: react-infinite-scroll-component
  • 原因:实现简单,数据量可控,交互组件需要保持在 DOM 中
// 社交媒体信息流适合传统方案
<InfiniteScroll
  dataLength={posts.length}
  next={loadMorePosts}
  hasMore={hasMorePosts}
  loader={<PostSkeleton />}
>
  {posts.map(post => (
    <PostCard 
      key={post.id} 
      post={post}
      onLike={handleLike}
      onComment={handleComment}
    />
  ))}
</InfiniteScroll>

场景 2:大型数据表格

  • 数据量:大(数千到数万条)
  • 交互:排序、筛选、编辑
  • ✅ 推荐: 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>

场景 3:聊天记录加载

  • 数据量:大(可能数万条消息)
  • 交互:滚动到特定消息、消息搜索
  • ✅ 推荐: 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-componentreact-infinite-scrollerreact-window-infinite-loader
渲染方式传统全量渲染传统全量渲染虚拟化渲染
性能表现500 条以内流畅500 条以内流畅10 万 + 条流畅
内存占用随数据量增长随数据量增长基本恒定
学习成本低低中高
容器支持容器/窗口容器/窗口容器为主
加载状态内置 loader内置 loader手动管理
维护状态活跃较低活跃(依托 react-window)
适用场景内容流、评论列表旧项目维护大数据表格、聊天记录

💡 选型建议

选择 react-infinite-scroll-component 如果:

  • 数据量预计在 500 条以内
  • 需要快速实现,开发时间紧张
  • 团队对虚拟化技术不熟悉
  • 场景是社交媒体、新闻列表等内容流

选择 react-infinite-scroller 如果:

  • 维护已有使用该库的项目
  • 有特定的兼容性需求
  • 新项目不建议选择此方案

选择 react-window-infinite-loader 如果:

  • 数据量可能超过 1000 条
  • 对性能和内存占用有严格要求
  • 需要支持大数据集(表格、聊天记录等)
  • 团队有能力处理稍复杂的技术方案

🎯 最后的建议

无限滚动看似简单,但技术选型会影响长期维护成本。数据量是核心判断标准——500 条以下可以用简单方案,超过这个阈值就应该考虑虚拟化。同时要考虑团队技术栈和用户设备性能,移动端用户对性能更敏感。

如果项目刚开始且不确定数据规模,建议直接采用 react-window-infinite-loader,虽然学习成本稍高,但能避免后期性能问题导致的重构。

如何选择: react-infinite-scroll-component vs react-window-infinite-loader vs react-infinite-scroller

  • react-infinite-scroll-component:

    选择 react-infinite-scroll-component 如果你需要快速实现基础无限滚动功能,且数据量不大(几百条以内)。它 API 简单,学习成本低,适合内容流、评论列表等场景。但要注意它不采用虚拟化技术,大量数据时会有性能问题。

  • react-window-infinite-loader:

    选择 react-window-infinite-loader 如果你需要处理成千上万条数据,或对性能有严格要求。它采用虚拟化技术,只渲染可见区域,内存占用低。适合数据表格、大型列表、聊天记录等场景,但学习曲线较陡峭。

  • react-infinite-scroller:

    选择 react-infinite-scroller 仅用于维护旧项目。该包已不再积极维护,官方文档显示其功能已被其他方案替代。新项目不建议使用,应优先考虑 react-infinite-scroll-component 或基于虚拟化的解决方案。

react-infinite-scroll-component的README

react-infinite-scroll-component npm npm bundlephobia

All Contributors

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.

Install

npm install react-infinite-scroll-component
# or
yarn add react-infinite-scroll-component
# or
pnpm add react-infinite-scroll-component

Two APIs

APIWhen to use
InfiniteScroll componentMost cases, handles loader, endMessage, pull-to-refresh, inverse scroll UI
useInfiniteScroll hookCustom UI, you own the markup, the hook manages the observer

InfiniteScroll component

Basic usage (TypeScript)

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>
  );
}

Scroll inside a fixed-height container

<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>;

Inverse scroll (chat / messaging UIs)

<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>

Pull-to-refresh

<InfiniteScroll
  dataLength={items.length}
  next={fetchMore}
  hasMore={hasMore}
  loader={<p>Loading...</p>}
  pullDownToRefresh
  pullDownToRefreshThreshold={50}
  refreshFunction={refreshList}
  pullDownToRefreshContent={
    <h3 style={{ textAlign: 'center' }}>&#8595; Pull down to refresh</h3>
  }
  releaseToRefreshContent={
    <h3 style={{ textAlign: 'center' }}>&#8593; Release to refresh</h3>
  }
>
  {items.map((item) => (
    <div key={item.id}>{item.name}</div>
  ))}
</InfiniteScroll>

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.

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>
  );
}

Framework recipes

Next.js App Router

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>
  );
}

With TanStack Query

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>
  );
}

With SWR

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>
  );
}

Three scroll modes

ModeHow to useUse case
Window scrollOmit height and scrollableTargetSocial feeds, blogs, product listings
Fixed-height containerPass height propEmbedded lists, sidebars
Custom scrollable parentPass scrollableTarget (element or id)Existing overflow containers

Props, InfiniteScroll

PropTypeRequiredDefaultDescription
dataLengthnumberyes-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() => voidyes-Called once when the sentinel enters the viewport. Append new items to your list state inside this callback; do not replace the existing items.
hasMorebooleanyes-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.
loaderReactNodeyes-Rendered below the list while the next page is loading. Displayed between the last item and the bottom sentinel.
endMessageReactNodeno-Rendered below the list when hasMore is false. Use it for an "all caught up" or "no more items" message.
heightnumber | stringno-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.
scrollableTargetHTMLElement | string | nullno-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.
scrollThresholdnumber | stringno0.8How 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.
inversebooleannofalseReverse 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.
pullDownToRefreshbooleannofalseEnable pull-to-refresh gesture on touch and mouse. Requires refreshFunction to also be set.
refreshFunction() => voidno-Called once when the user pulls down past pullDownToRefreshThreshold pixels and releases. Only active when pullDownToRefresh is true.
pullDownToRefreshThresholdnumberno100How many pixels the user must pull down before refreshFunction is triggered on release.
pullDownToRefreshContentReactNodeno-Content shown inside the pull-to-refresh area while the user is pulling but has not yet reached the threshold.
releaseToRefreshContentReactNodeno-Content shown inside the pull-to-refresh area once the threshold is passed and the user can release to refresh.
onScroll(e: UIEvent) => voidno-Callback fired on every scroll event on the container. Receives the native UIEvent. Useful for syncing UI state with scroll position.
classNamestringno''CSS class name applied to the inner scroll container div.
styleCSSPropertiesno-Inline style object applied to the inner scroll container div. Merged with the component's default layout styles.
roleAriaRoleno-Semantic role for the scroll container. Use "list" for item lists, "feed" for activity streams.
tabIndexnumberno-Makes the scroll container focusable. Pass 0 to include it in the natural tab sequence.
idstringno-DOM id for the container. Useful when other elements reference it via aria-labelledby.
aria-*AriaAttributesno-Any React aria-* prop (aria-label, aria-labelledby, aria-describedby, etc.) forwarded to the scroll container.
hasChildrenbooleanno-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.
initialScrollYnumberno-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:

<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>}
>

Props, useInfiniteScroll

PropTypeRequiredDefaultDescription
dataLengthnumberyes-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() => voidyes-Called once when the sentinel enters the viewport. Append new items to your list state inside this callback; do not replace the existing items.
hasMorebooleanyes-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.
scrollThresholdnumber | stringno0.8How 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.
scrollableTargetHTMLElement | string | nullno-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.
inversebooleannofalseWhen 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
    • Edit yk7637p62z
  • infinite scroll till 500 elements, window scroll
    • Edit 439v8rmqm0
  • infinite scroll in an element (height 400px)
    • Edit w3w89k7x8
  • infinite scroll with scrollableTarget
    • Edit r7rp40n0zm

Contributors ✨

Thanks goes to these wonderful people (emoji key):

Ankeet Maini
Ankeet Maini

💬 📖 💻 👀 🚧
Darsh Shah
Darsh Shah

🚇 💻 👀 🚧
Eliya Cohen
Eliya Cohen

💻
Nitin Kukreja
Nitin Kukreja

💻
Bruno Sabetta
Bruno Sabetta

💻 📖
Osmar Pérez Bautista
Osmar Pérez Bautista

💻
Shreya Dahal
Shreya Dahal

💻
Vlad Harahan
Vlad Harahan

💻 📖
Daniel Caldas
Daniel Caldas

💻
Alaeddine Douagi
Alaeddine Douagi

💻
Carlos
Carlos

💻
Championrunner
Championrunner

📖
Daniel Sogl
Daniel Sogl

💻
Darren Oster
Darren Oster

💻
Illia Panasenko
Illia Panasenko

💻
Kiko Beats
Kiko Beats

💻
Matt Trussler
Matt Trussler

💻
Nimit Suwannagate
Nimit Suwannagate

💻
Rajat
Rajat

💻
Rich
Rich

💻
Ritesh Goyal
Ritesh Goyal

💻
babycannotsay
babycannotsay

💻
cesco
cesco

💻
Harry
Harry

💻
ludwig404
ludwig404

💻
Karl Johansson
Karl Johansson

💻
Geoffrey Teng
Geoffrey Teng

💻
Sayed Risat
Sayed Risat

💻 📖

This project follows the all-contributors specification. Contributions of any kind are welcome!

LICENSE

MIT