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-infinite-scroll-component、react-infinite-scroller、react-window-infinite-loader は、React で無限スクロールを実装する代表的なライブラリですが、根本的なアプローチが異なります。実際の開発現場で直面する問題にどう対応するか、技術的な観点から比較します。
react-infinite-scroll-component は DOM にすべてのアイテムをレンダリングします。
// react-infinite-scroll-component: 全アイテムをレンダリング
import InfiniteScroll from 'react-infinite-scroll-component';
function UserList({ users, fetchMore, hasMore }) {
return (
<InfiniteScroll
dataLength={users.length}
next={fetchMore}
hasMore={hasMore}
loader={<h4>読み込み中...</h4>}
>
{users.map(user => (
<div key={user.id}>{user.name}</div>
))}
</InfiniteScroll>
);
}
react-infinite-scroller も同様に全アイテムをレンダリングします。
// react-infinite-scroller: 全アイテムをレンダリング
import InfiniteScroll from 'react-infinite-scroller';
function UserList({ users, fetchMore, hasMore }) {
return (
<InfiniteScroll
pageStart={0}
loadMore={fetchMore}
hasMore={hasMore}
loader={<div key={0}>読み込み中...</div>}
>
{users.map(user => (
<div key={user.id}>{user.name}</div>
))}
</InfiniteScroll>
);
}
react-window-infinite-loader は仮想化を使用して表示中のアイテムのみをレンダリングします。
react-window と連携して高性能を実現// react-window-infinite-loader: 仮想化で表示中のみのレンダリング
import InfiniteLoader from 'react-window-infinite-loader';
import { FixedSizeList } from 'react-window';
function UserList({ users, fetchMore, hasMore }) {
const isItemLoaded = index => !hasMore || index < users.length;
return (
<InfiniteLoader
isItemLoaded={isItemLoaded}
itemCount={hasMore ? users.length + 1 : users.length}
loadMoreItems={fetchMore}
>
{({ onItemsRendered, ref }) => (
<FixedSizeList
height={600}
itemCount={users.length}
itemSize={50}
onItemsRendered={onItemsRendered}
ref={ref}
>
{({ index, style }) => (
<div style={style} key={users[index]?.id}>
{users[index]?.name || '読み込み中...'}
</div>
)}
</FixedSizeList>
)}
</InfiniteLoader>
);
}
データの状態管理方法もライブラリ間で異なります。
react-infinite-scroll-component は親コンポーネントで状態を管理します。
dataLength props で現在のアイテム数を通知// react-infinite-scroll-component: 親で状態管理
function UserList() {
const [users, setUsers] = useState([]);
const [page, setPage] = useState(1);
const fetchMore = async () => {
const newUsers = await api.getUsers(page + 1);
setUsers([...users, ...newUsers]);
setPage(page + 1);
};
return (
<InfiniteScroll
dataLength={users.length}
next={fetchMore}
hasMore={users.length < total}
>
{users.map(user => <UserCard key={user.id} user={user} />)}
</InfiniteScroll>
);
}
react-infinite-scroller も同様に親コンポーネントで状態を管理します。
pageStart で現在のページを追跡// react-infinite-scroller: 親で状態管理
function UserList() {
const [users, setUsers] = useState([]);
const [page, setPage] = useState(0);
const fetchMore = async () => {
const newUsers = await api.getUsers(page + 1);
setUsers([...users, ...newUsers]);
setPage(page + 1);
};
return (
<InfiniteScroll
pageStart={page}
loadMore={fetchMore}
hasMore={users.length < total}
>
{users.map(user => <UserCard key={user.id} user={user} />)}
</InfiniteScroll>
);
}
react-window-infinite-loader はアイテムのロード状態をライブラリが管理します。
isItemLoaded で各アイテムのロード状態を判定loadMoreItems がバッチ処理をサポート// react-window-infinite-loader: ライブラリがロード状態を管理
function UserList() {
const [users, setUsers] = useState({});
const isItemLoaded = index => users[index] !== undefined;
const loadMoreItems = async ({ startIndex, stopIndex }) => {
const newUsers = await api.getUsers(startIndex, stopIndex);
setUsers(prev => ({ ...prev, ...newUsers }));
};
return (
<InfiniteLoader
isItemLoaded={isItemLoaded}
itemCount={total}
loadMoreItems={loadMoreItems}
>
{({ onItemsRendered, ref }) => (
<FixedSizeList
height={600}
itemCount={total}
itemSize={50}
onItemsRendered={onItemsRendered}
ref={ref}
>
{({ index, style }) => (
<div style={style} key={index}>
{users[index]?.name || '読み込み中...'}
</div>
)}
</FixedSizeList>
)}
</InfiniteLoader>
);
}
パフォーマンスは無限スクロール実装で最も重要な考慮事項です。
react-infinite-scroll-component のパフォーマンス特性:
// react-infinite-scroll-component: 大量データでの注意点
function UserList() {
const [users, setUsers] = useState([]);
// ⚠️ 10000 件を超えるとパフォーマンス低下のリスク
const fetchMore = async () => {
if (users.length > 5000) {
console.warn('パフォーマンス低下の可能性があります');
}
const newUsers = await api.getUsers(users.length);
setUsers([...users, ...newUsers]);
};
return (
<InfiniteScroll
dataLength={users.length}
next={fetchMore}
hasMore={true}
scrollThreshold={0.9} // 90% スクロールでトリガー
>
{users.map(user => <UserCard key={user.id} user={user} />)}
</InfiniteScroll>
);
}
react-infinite-scroller のパフォーマンス特性:
react-infinite-scroll-component と同様の制限// react-infinite-scroller: 性能制限への対応
function UserList() {
const [users, setUsers] = useState([]);
const fetchMore = async () => {
// ⚠️ 大量データでは仮想化ライブラリの使用を検討
if (users.length > 3000) {
console.warn('react-window-infinite-loader への移行を検討してください');
}
const newUsers = await api.getUsers(users.length);
setUsers([...users, ...newUsers]);
};
return (
<InfiniteScroll
pageStart={0}
loadMore={fetchMore}
hasMore={users.length < total}
useWindow={true} // window スクロールを使用
>
{users.map(user => <UserCard key={user.id} user={user} />)}
</InfiniteScroll>
);
}
react-window-infinite-loader のパフォーマンス特性:
// react-window-infinite-loader: 大規模データに最適化
function UserList() {
const [users, setUsers] = useState({});
const TOTAL_ITEMS = 100000;
const isItemLoaded = index => users[index] !== undefined;
const loadMoreItems = async ({ startIndex, stopIndex }) => {
// バッチ処理で効率的にデータ取得
const newUsers = await api.getUsersBatch(startIndex, stopIndex);
setUsers(prev => ({ ...prev, ...newUsers }));
};
return (
<InfiniteLoader
isItemLoaded={isItemLoaded}
itemCount={TOTAL_ITEMS}
loadMoreItems={loadMoreItems}
minimumBatchSize={10} // 一度にロードする最小アイテム数
>
{({ onItemsRendered, ref }) => (
<FixedSizeList
height={600}
itemCount={TOTAL_ITEMS}
itemSize={50}
onItemsRendered={onItemsRendered}
ref={ref}
overscanCount={5} // 表示範囲外のバッファ
>
{({ index, style }) => (
<div style={style} key={index}>
{users[index]?.name || '読み込み中...'}
</div>
)}
</FixedSizeList>
)}
</InfiniteLoader>
);
}
実務ではページ遷移後にスクロール位置を保持する必要がある場合があります。
react-infinite-scroll-component はスクロール位置の保持が容易です。
scrollTop を直接制御可能// react-infinite-scroll-component: スクロール位置保持
function UserList() {
const scrollRef = useRef(null);
const [scrollPosition, setScrollPosition] = useState(0);
useEffect(() => {
if (scrollRef.current) {
scrollRef.current.scrollTop = scrollPosition;
}
}, []);
const handleScroll = () => {
if (scrollRef.current) {
setScrollPosition(scrollRef.current.scrollTop);
}
};
return (
<div
ref={scrollRef}
onScroll={handleScroll}
style={{ height: '600px', overflow: 'auto' }}
>
<InfiniteScroll
dataLength={users.length}
next={fetchMore}
hasMore={hasMore}
scrollableTarget={scrollRef.current}
>
{users.map(user => <UserCard key={user.id} user={user} />)}
</InfiniteScroll>
</div>
);
}
react-infinite-scroller も同様にスクロール位置を制御できます。
useWindow props で window/要素スクロールを切り替えgetScrollParent で制御// react-infinite-scroller: スクロール位置保持
function UserList() {
const containerRef = useRef(null);
useEffect(() => {
if (containerRef.current) {
containerRef.current.scrollTop = savedPosition;
}
}, []);
return (
<div ref={containerRef} style={{ height: '600px', overflow: 'auto' }}>
<InfiniteScroll
pageStart={0}
loadMore={fetchMore}
hasMore={hasMore}
useWindow={false} // 要素スクロールを使用
getScrollParent={() => containerRef.current}
>
{users.map(user => <UserCard key={user.id} user={user} />)}
</InfiniteScroll>
</div>
);
}
react-window-infinite-loader は仮想化のためスクロール位置の制御が特殊です。
scrollToItem メソッドを使用// react-window-infinite-loader: スクロール位置保持
function UserList() {
const listRef = useRef(null);
const targetIndex = useRef(0);
useEffect(() => {
if (listRef.current && targetIndex.current > 0) {
listRef.current.scrollToItem(targetIndex.current, 'start');
}
}, [users]);
return (
<InfiniteLoader
isItemLoaded={isItemLoaded}
itemCount={total}
loadMoreItems={loadMoreItems}
>
{({ onItemsRendered, ref }) => (
<FixedSizeList
height={600}
itemCount={total}
itemSize={50}
onItemsRendered={onItemsRendered}
ref={listRef}
>
{({ index, style }) => (
<div style={style} key={index}>
{users[index]?.name || '読み込み中...'}
</div>
)}
</FixedSizeList>
)}
</InfiniteLoader>
);
}
ローディング状態や終了時の表示も重要な UX 要素です。
react-infinite-scroll-component のカスタマイズ:
loader props でローディング表示を指定endMessage props で終了メッセージを表示// react-infinite-scroll-component: UI カスタマイズ
<InfiniteScroll
dataLength={users.length}
next={fetchMore}
hasMore={hasMore}
loader={
<div className="loader">
<Spinner />
<p>データを読み込んでいます...</p>
</div>
}
endMessage={
<div className="end-message">
<p>🎉 全てのデータを表示しました</p>
</div>
}
pullDownToRefresh={true}
refreshFunction={handleRefresh}
>
{users.map(user => <UserCard key={user.id} user={user} />)}
</InfiniteScroll>
react-infinite-scroller のカスタマイズ:
loader props でローディング表示を指定// react-infinite-scroller: UI カスタマイズ
<InfiniteScroll
pageStart={0}
loadMore={fetchMore}
hasMore={hasMore}
loader={
<div className="loader" key={0}>
<Spinner />
<p>データを読み込んでいます...</p>
</div>
}
>
{users.map(user => <UserCard key={user.id} user={user} />)}
{!hasMore && (
<div className="end-message" key="end">
<p>🎉 全てのデータを表示しました</p>
</div>
)}
</InfiniteScroll>
react-window-infinite-loader のカスタマイズ:
// react-window-infinite-loader: UI カスタマイズ
<InfiniteLoader
isItemLoaded={isItemLoaded}
itemCount={hasMore ? users.length + 1 : users.length}
loadMoreItems={fetchMore}
>
{({ onItemsRendered, ref }) => (
<FixedSizeList
height={600}
itemCount={hasMore ? users.length + 1 : users.length}
itemSize={50}
onItemsRendered={onItemsRendered}
ref={ref}
>
{({ index, style }) => {
if (index >= users.length) {
return (
<div style={style} key="loader">
<Spinner />
<p>データを読み込んでいます...</p>
</div>
);
}
return (
<div style={style} key={users[index].id}>
<UserCard user={users[index]} />
</div>
);
}}
</FixedSizeList>
)}
</InfiniteLoader>
これらのライブラリには多くの共通点もあります。
// 3 ライブラリ共通:React コンポーネントとして使用
function UserList({ users, fetchMore, hasMore }) {
// どのライブラリでも同様のパターンで使用可能
return (
<InfiniteScrollComponent
// 各ライブラリの props を設定
>
{users.map(user => <UserCard key={user.id} user={user} />)}
</InfiniteScrollComponent>
);
}
useWindow または scrollableTarget で制御// window スクロールの場合
<InfiniteScroll useWindow={true}>
{/* 内容 */}
</InfiniteScroll>
// 要素スクロールの場合
<div style={{ height: '600px', overflow: 'auto' }}>
<InfiniteScroll useWindow={false}>
{/* 内容 */}
</InfiniteScroll>
</div>
next、loadMore、loadMoreItems でデータ取得をトリガー// 3 ライブラリ共通:非同期データ読み込みパターン
const fetchMore = async () => {
try {
const newItems = await api.getData(page);
setItems([...items, ...newItems]);
} catch (error) {
console.error('データ取得エラー:', error);
// エラー状態の管理は開発者側で
}
};
// TypeScript での使用例(3 ライブラリ共通)
interface User {
id: number;
name: string;
}
interface Props {
users: User[];
fetchMore: () => Promise<void>;
hasMore: boolean;
}
function UserList({ users, fetchMore, hasMore }: Props) {
// 型チェックが機能
}
scrollThreshold props で制御// スクロール閾値の設定(対応ライブラリ)
<InfiniteScroll
scrollThreshold={0.8} // 80% スクロールでトリガー
// 早期読み込みでユーザー待機時間を削減
>
{/* 内容 */}
</InfiniteScroll>
| 機能 | react-infinite-scroll-component | react-infinite-scroller | react-window-infinite-loader |
|---|---|---|---|
| 仮想化 | ❌ 全アイテムレンダリング | ❌ 全アイテムレンダリング | ✅ 表示中のみのレンダリング |
| 最大推奨アイテム数 | ~1000 件 | ~1000 件 | 10 万件以上 |
| メモリ使用量 | データ量に比例 | データ量に比例 | 一定 |
| 実装の複雑さ | 低 | 低 | 中〜高 |
| スクロール位置保持 | 容易 | 容易 | 特殊な制御が必要 |
| メンテナンス状況 | 活発 | 要注意 | 活発 |
| 依存関係 | 軽量 | 軽量 | react-window が必要 |
データ量とパフォーマンス要件で選択しましょう:
react-infinite-scroll-component(シンプルで十分)react-infinite-scroller(変更コストを考慮)react-window-infinite-loader(性能が最優先)react-window-infinite-loader(安定性と拡張性)重要なポイント:無限スクロール実装では、初期の開発速度よりも長期的なパフォーマンスとメンテナンス性を重視すべきです。プロジェクトの規模と成長を見据えて、適切なライブラリを選択しましょう。
react-infinite-scroll-component を選択するのは、シンプルな実装で十分な場合や、仮想化の複雑さを避けたいプロジェクトです。メンテナンスが活発で API が直感的ですが、大量のデータ(1000 件以上)をレンダリングするとパフォーマンスが低下する可能性があります。小〜中規模のリストや、プロトタイプ開発に適しています。
react-infinite-scroller を選択するのは、軽量な依存関係とシンプルな API を優先する場合です。ただし、メンテナンス状況に注意が必要で、新しいプロジェクトでは慎重に評価すべきです。既存プロジェクトの維持や、仮想化が不要なシンプルなユースケースに適しています。
react-window-infinite-loader を選択するのは、大規模なデータセット(数千件以上)を扱う場合や、パフォーマンスが最優先事項の場合です。react-window と連携して仮想化を実現し、表示されているアイテムのみをレンダリングするため、メモリ使用量と描画コストを大幅に削減できます。エンタープライズアプリケーションやデータ密集型プロジェクトに最適です。
A component to make all your infinite scrolling woes go away with just 4.15 kB! Pull Down to Refresh feature
added. An infinite-scroll that actually works and super-simple to integrate!
npm install --save react-infinite-scroll-component
or
yarn add react-infinite-scroll-component
// in code ES6
import InfiniteScroll from 'react-infinite-scroll-component';
// or commonjs
var InfiniteScroll = require('react-infinite-scroll-component');
<InfiniteScroll
dataLength={items.length} //This is important field to render the next data
next={fetchData}
hasMore={true}
loader={<h4>Loading...</h4>}
endMessage={
<p style={{ textAlign: 'center' }}>
<b>Yay! You have seen it all</b>
</p>
}
// below props only if you need pull down functionality
refreshFunction={refresh}
pullDownToRefresh
pullDownToRefreshThreshold={50}
pullDownToRefreshContent={
<h3 style={{ textAlign: 'center' }}>↓ Pull down to refresh</h3>
}
releaseToRefreshContent={
<h3 style={{ textAlign: 'center' }}>↑ Release to refresh</h3>
}
>
{items}
</InfiniteScroll>
<div
id="scrollableDiv"
style={{
height: 300,
overflow: 'auto',
display: 'flex',
flexDirection: 'column-reverse',
}}
>
{/*Put the scroll bar always on the bottom*/}
<InfiniteScroll
dataLength={items.length}
next={fetchMoreData}
style={{ display: 'flex', flexDirection: 'column-reverse' }} //To put endMessage and loader to the top.
inverse={true}
hasMore={true}
loader={<h4>Loading...</h4>}
scrollableTarget="scrollableDiv"
>
{items.map((_, index) => (
<div style={style} key={index}>
div - #{index}
</div>
))}
</InfiniteScroll>
</div>
The InfiniteScroll component can be used in three ways.
height prop if you want your scrollable content to have a specific height, providing scrollbars for scrolling your content and fetching more data.scrollableTarget prop to reference the DOM element and use it's scrollbars for fetching more data.height or scrollableTarget props, the scroll will happen at document.body like Facebook's timeline scroll.next() is now triggered by an IntersectionObserver watching an invisible sentinel element at the bottom of the list (top for inverse mode), rather than a scroll event listener. This means:
throttle-debounce has been removed. The package now ships with zero runtime dependencies. The onScroll callback receives every scroll event directly without throttling.
scrollableTarget accepts HTMLElement directlyPreviously scrollableTarget only accepted a string element ID. It now accepts HTMLElement | string | null, so you can pass a ref value directly:
const ref = useRef(null);
// ...
<div ref={ref} style={{ height: 300, overflow: 'auto' }}>
<InfiniteScroll scrollableTarget={ref.current} ...>
{items}
</InfiniteScroll>
</div>
The component is now a React function component. The public prop API is unchanged — no migration needed.
scrollableTarget (a parent element which is scrollable)
| name | type | description |
|---|---|---|
| next | function | a function which must be called after reaching the bottom. It must trigger some sort of action which fetches the next data. The data is passed as children to the InfiniteScroll component and the data should contain previous items too. e.g. Initial data = [1, 2, 3] and then next load of data should be [1, 2, 3, 4, 5, 6]. |
| hasMore | boolean | it tells the InfiniteScroll component on whether to call next function on reaching the bottom and shows an endMessage to the user |
| children | node (list) | the data items which you need to scroll. |
| dataLength | number | set the length of the data.This will unlock the subsequent calls to next. |
| loader | node | you can send a loader component to show while the component waits for the next load of data. e.g. <h3>Loading...</h3> or any fancy loader element |
| scrollThreshold | number | string | A threshold value defining when InfiniteScroll will call next. Default value is 0.8. It means the next will be called when user comes below 80% of the total height. If you pass threshold in pixels (scrollThreshold="200px"), next will be called once you scroll at least (100% - scrollThreshold) pixels down. |
| onScroll | function | a function that will listen to the scroll event on the scrolling container. |
| endMessage | node | this message is shown to the user when he has seen all the records which means he's at the bottom and hasMore is false |
| className | string | add any custom class you want |
| style | object | any style which you want to override |
| height | number | optional, give only if you want to have a fixed height scrolling content |
| scrollableTarget | node or string | optional, reference to a (parent) DOM element that is already providing overflow scrollbars to the InfiniteScroll component. You should provide the id of the DOM node preferably. |
| hasChildren | bool | children is by default assumed to be of type array and it's length is used to determine if loader needs to be shown or not, if your children is not an array, specify this prop to tell if your items are 0 or more. |
| pullDownToRefresh | bool | to enable Pull Down to Refresh feature |
| pullDownToRefreshContent | node | any JSX that you want to show the user, default={<h3>Pull down to refresh</h3>} |
| releaseToRefreshContent | node | any JSX that you want to show the user, default={<h3>Release to refresh</h3>} |
| pullDownToRefreshThreshold | number | minimum distance the user needs to pull down to trigger the refresh, default=100px , a lower value may be needed to trigger the refresh depending your users browser. |
| refreshFunction | function | this function will be called, it should return the fresh data that you want to show the user |
| initialScrollY | number | set a scroll y position for the component to render with. |
| inverse | bool | set infinite scroll on top |
Thanks goes to these wonderful people (emoji key):
This project follows the all-contributors specification. Contributions of any kind are welcome!