tech-book-labs
ガイド(UI / インタラクション) · 2 セクション統合

React Virtuoso 仮想スクロール実装ガイド

React Virtuoso で大量データの仮想スクロールを組む実装ノート。基本の virtual list、グループ化 + sticky header の応用までを 1 ページに統合。

著者:TechBook.net編集部 · 最終検証 2026-05-10
セクション · 2026-05-10 · react-virtuoso 4.18.6

react-virtuoso で 100 万行を仮想スクロールする

react-virtuoso v4 で大量行リストを仮想スクロール化する最小コード、totalCount + itemContent パターン、よく詰まる点を触れる demo で確認する実装メモ。

検証日: 2026-05-10

使用バージョン: react-virtuoso@4.18.6

対象: 数千〜数百万行のリストを React で表示する場面、ログビューア、無限スクロール

react-virtuoso v4 で 大量行リストを仮想スクロール(画面に映る行 + 上下 buffer 分だけ DOM に保持)化するパターン。totalCount + itemContent の最小コード、無限スクロール(endReached)、動的行高の自動測定を動く demo で確認します。

触って試す

行数:10,000 行を仮想スクロール

行数を切替えて、100 万行でもスクロールが軽いことを確認できる。

なぜ仮想化が必要か

通常の <ul>{items.map(...)}</ul>全アイテムを DOM に存在させる

行数通常のレンダリング仮想スクロール
1,000余裕余裕
10,000重い(初期 500ms+)軽い
100,000フリーズ軽い(可視範囲 + バッファのみ DOM)
1,000,000クラッシュ軽い

仮想スクロールは 画面に映る行 + 上下 buffer 分だけ DOM に保持する。

Virtuoso の recycling サイクル — 1M 行あっても DOM は数十程度に収まる (クリックで拡大)

ポイントは DOM 数が「表示範囲 + 上下 buffer」だけで一定になること。totalCount を増やしても DOM 数は変わらず、スクロール時のメモリ・layout コストが scale-out しない。

最小サンプル

<Virtuoso>totalCountitemContent(index) を渡すだけ。スクロールは Virtuoso 自身が管理:

import { Virtuoso } from "react-virtuoso";

export function List({ rows }: { rows: { id: number; text: string }[] }) {
  return (
    <Virtuoso
      style={{ height: 400 }}
      totalCount={rows.length}
      itemContent={(index) => <div>{rows[index].text}</div>}
    />
  );
}

API が小さい:totalCount で総数、itemContent(index) で 1 行 JSX。

高さの扱い

Virtuoso は 行の高さがバラバラでも自動計算 する(動的高さに対応)。固定高でない場合も特に props を足す必要なし。

ただしパフォーマンス重視なら defaultItemHeight で初期推定値を渡すと、初回レイアウトのジャンプが減る。

無限スクロール(API ページネーション)

endReached callback で末尾到達を検知 → API から次ページを fetch → state に append するパターン:

const [rows, setRows] = useState<Row[]>([]);

<Virtuoso
  style={{ height: 400 }}
  data={rows}
  endReached={async () => {
    const next = await fetchNextPage(rows.length);
    setRows((prev) => [...prev, ...next]);
  }}
  itemContent={(index, row) => <div>{row.text}</div>}
/>

endReached で末尾に到達した時に追加 fetch。バッファ範囲も自動で調整される。

endReached による無限スクロール — load 中もレイアウトが安定する (クリックで拡大)

ポイント:

  • endReached の発火タイミングは「末尾の endReached 内側 buffer に到達したとき」(デフォルト最後の行に到達直前)。fetch latency の余裕を作るために overscan を増やすと早めに firing する
  • 重複 fetch を防ぐ:fetch 中フラグを置いて、endReached の内側で if (loading) return する

分類別コンポーネント

Component用途
Virtuoso縦リスト(基本)
TableVirtuoso<table> 構造の仮想化
GridVirtuosoグリッド表示
VirtuosoMessageListチャット系(末尾追従、過去ロード)
GroupedVirtuososticky group header 付き — /articles/react-virtuoso-grouped-sticky/ で深掘り

つまずいたポイント

  • 親要素の高さが必須:style={{ height: ... }} を渡さないと 0 で何も表示されない。flex 親なら flex: 1
  • 同じデータで再描画:data の参照が変わらないと追加されたと認識されないので、setRows([...prev, ...next]) のように新配列を作る
  • server-render しない:Astro / Next.js では client:load で hydrate(SSR の出力は空コンテナ + 後から hydrate)
  • 横スクロールが必要なケース:itemContent 内で overflow-x: auto を当てる。Virtuoso 自身は縦のみ
  • scroll position の維持:タブ切替や戻る操作での scroll 位置は、ref={virtuosoRef} + scrollToIndex(index) で復元

評価

観点評価コメント
学習コストtotalCount + itemContent の 2 つで開始可能
動的行高計測自動、設定不要
パフォーマンス100 万行でも 60fps
無限スクロールendReached で組み込み対応
バンドル30KB 前後(min+gzip)

向く / 向かないケース

  • 向く: ログビューア、SNS タイムライン、大量検索結果、数千行 admin テーブル
  • 向かない: 数十行のリスト(オーバーキル)、複雑な grid / sticky header(Grouped/Table 版を選ぶ)
  • 向かない: print したい場合(仮想化されているので print 時に全行は出ない)

関連 Topic / 関連書籍

この記事と関係する tech-book.net の Topic と、それぞれの Topic に紐づく書籍:

tech-book.net /books/9784839966645

React Native+Expoではじめるスマホアプリ開発 : JavaScriptによるアプリ構築の実際

松澤 太郎 · マイナビ出版 · 2018年

「React Native」は、Facebookが開発しているスマートフォンアプリ向けの開発環境です。ほとんどのコードをJav…

詳細を tech-book.net で見る
tech-book.net /books/9784873119380

Reactハンズオンラーニング 第2版 : Webアプリケーション開発のベストプラクティス

Alex Banks/Eve Porcello/宮崎 空 · オライリー・ジャパン · 2021年 · ¥3,740

Webフロントエンドの「今」を学びたい人へ! Facebookが開発したJavaScrip…

詳細を tech-book.net で見る
tech-book.net /books/9784873117881

Reactビギナーズガイド : コンポーネントベースのフロントエンド開発入門

Stoyan Stefanov/牧野 聡 · オライリー・ジャパン · 2017年 · ¥2,750

FacebookのエンジニアによるReactの入門書! ReactによるコンポーネントベースのWebフロントエンド開発の…

詳細を tech-book.net で見る
tech-book.net /books/9784297129163

TypeScriptとReact/Next.jsでつくる実践Webアプリケーション開発

手島 拓也/吉田 健人/高林 佳稀 · 技術評論社 · 2022年

新しいフロントエンドの入門書決定版! 本書はReact/Next.jsとTypeScriptを用いてWebアプリケーションを開…

詳細を tech-book.net で見る
tech-book.net /books/9784844379546

【POD】React &amp; Gatsby開発入門

竹本 雄貴
詳細を tech-book.net で見る
セクション · 2026-05-10 · react-virtuoso 4.18.6

react-virtuoso で group + sticky header + scrollToIndex

react-virtuoso の踏み込んだ機能 — GroupedVirtuoso で sticky group header、groupCounts による平坦化と group 復元、scrollToIndex / scrollIntoView で外部から位置制御するパターンを触れる demo で確認する実装メモ。

検証日: 2026-05-10

使用バージョン: react-virtuoso@4.18.6

対象: 入門は通っている、グループ化された大量データ(チーム別ユーザー一覧 / 月別取引履歴 等)を仮想スクロールで扱いたい人

react-virtuoso の踏み込んだ機能 — GroupedVirtuoso で sticky group header、groupCounts による平坦化と group 復元、scrollToIndex / scrollIntoView で外部から位置制御するパターンを動く demo で確認します。

触って試す

上のボタンで該当チームへジャンプ + sticky header(チーム名)が画面上部に貼り付く。

1. groupCounts で構造を渡す

GroupedVirtuosogroup ごとの行数配列(groupCounts) で構造を表現する:

import { GroupedVirtuoso } from "react-virtuoso";

const groups = [
  { team: "Frontend", rows: [...] },  // 50 行
  { team: "Backend", rows: [...] },   // 80 行
  { team: "DevOps", rows: [...] },    // 30 行
];

const groupCounts = groups.map((g) => g.rows.length);  // [50, 80, 30]
const flatRows = groups.flatMap((g) => g.rows);         // 平坦な 160 行配列

<GroupedVirtuoso
  style={{ height: 360 }}
  groupCounts={groupCounts}
  groupContent={(groupIndex) => (
    <div className="px-3 py-2 font-semibold sticky-header">
      {groups[groupIndex].team}
    </div>
  )}
  itemContent={(itemIndex) => (
    <div>{flatRows[itemIndex].name}</div>
  )}
/>

ポイント:

  • groupCounts: number[] で group の構造を表現(各要素 = その group の行数)
  • groupContent(groupIndex) = group header(自動で sticky になる)
  • itemContent(itemIndex) = 平坦化した行 index で各行を render
  • flat ↔ grouped の変換 は呼び出し側で(virtuoso 側が裁定はしない)
groupCounts による flat index ↔ group index のマッピング (クリックで拡大)

2. scrollToIndex で外部から位置制御

Virtuosoref 経由で命令的に scroll:

import { useRef } from "react";
import { GroupedVirtuoso, type VirtuosoHandle } from "react-virtuoso";

const ref = useRef<VirtuosoHandle>(null);

// 200 行目に飛ぶ
ref.current?.scrollToIndex({ index: 200, align: "start" });

align:

  • "start"(画面上端に index を合わせる)
  • "center"(中央)
  • "end"(下端)
align の挙動 — viewport のどこに target index を貼り付けるか (クリックで拡大)

実用ヒント:

  • 「次のグループ先頭」UIalign: "start"(header が上端に sticky)
  • 「メッセージ詳細にフォーカス」align: "center"(target を中央に)
  • 「最新まで一気にスクロール」align: "end" + behavior: "smooth"

behavior: "smooth" を渡せばスムーススクロール:

ref.current?.scrollToIndex({ index: 500, align: "center", behavior: "smooth" });

3. group 開始 index を計算してジャンプ

「Backend グループの先頭へ」のような UI には、group ごとの先頭 flat index を計算しておく:

const groupStartIndices = useMemo(() => {
  const out: number[] = [];
  let acc = 0;
  for (const g of groups) {
    out.push(acc);
    acc += g.rows.length;
  }
  return out;
}, [groups]);

// Backend(index=1) の先頭へ
ref.current?.scrollToIndex({ index: groupStartIndices[1], align: "start" });

4. 動的に行が増減する場面(遅延更新)

サーバから随時 fetch してくる場合は firstItemIndex を一緒に渡すと、頭追加でも scroll 位置がずれない:

<Virtuoso
  firstItemIndex={firstItemIndex}   // 上に load した時に減らす
  initialTopMostItemIndex={500}     // 起動時の初期位置
  data={rows}
  itemContent={(index, row) => <Row row={row} />}
/>

「過去ログを上に load」のチャット系 UI で必須。

5. 動的行高(明示しなくても自動測定)

Virtuoso行高を測定しながら描画する。固定でも可変でも OK。 ただし 画像が後から load される行 は初回測定が短すぎることがあるので、onLoadforceUpdate を呼ぶか、画像に aspect-ratio CSS を当てて事前に高さを確保。

<img
  src={src}
  loading="lazy"
  style={{ aspectRatio: "16 / 9", width: "100%" }}
/>

6. Group header のカスタマイズ

groupContent の中で z-index / shadow / 色を CSS で:

groupContent={(groupIndex) => (
  <div
    className="px-3 py-2 text-xs font-semibold"
    style={{
      background: "var(--color-paper)",
      borderBottom: "1px solid var(--color-line)",
      borderTop: "1px solid var(--color-line)",
    }}
  >
    {groups[groupIndex].team}
    <span className="ml-2 font-mono">({groups[groupIndex].rows.length})</span>
  </div>
)}

GroupedVirtuoso は header に 自動で sticky 効果を当てる(自前で position: sticky 不要)。

7. 関連 Component の使い分け

Component用途
Virtuoso縦リスト基本(group なし)
GroupedVirtuosoグループ化 + sticky header
TableVirtuoso<table> 構造の仮想化(thead sticky)
GridVirtuosoグリッド表示
VirtuosoMessageListチャット系(末尾追従、過去ロード)

「sticky header + 大量行 + 動的行高 + 末尾追従」が必要なら virtuoso 系がほぼ唯一の現実解。

つまずいたポイント

  • groupCounts が空配列:GroupedVirtuoso が何も描画しない。groupCounts.length === 0 ? <Empty /> : <GroupedVirtuoso ... /> で出し分け
  • groupContent の高さが毎回違う:測定タイミングが揺れて scroll が跳ねる。固定 height にするか line-height 揃える
  • scrollToIndex 直後に再描画:行の高さが未測定だと正確な位置に飛ばない、scrollToIndexsetTimeout(0) で 1 tick 遅延
  • server-render しない:Astro/Next.js は client:load で hydrate
  • horizontal scroll が必要:Virtuoso は縦のみ、itemContent 内で overflow-x: auto する div を作る

関連 Topic / 関連書籍

この記事と関係する tech-book.net の Topic と、それぞれの Topic に紐づく書籍:

tech-book.net /books/9784839966645

React Native+Expoではじめるスマホアプリ開発 : JavaScriptによるアプリ構築の実際

松澤 太郎 · マイナビ出版 · 2018年

「React Native」は、Facebookが開発しているスマートフォンアプリ向けの開発環境です。ほとんどのコードをJav…

詳細を tech-book.net で見る
tech-book.net /books/9784873119380

Reactハンズオンラーニング 第2版 : Webアプリケーション開発のベストプラクティス

Alex Banks/Eve Porcello/宮崎 空 · オライリー・ジャパン · 2021年 · ¥3,740

Webフロントエンドの「今」を学びたい人へ! Facebookが開発したJavaScrip…

詳細を tech-book.net で見る
tech-book.net /books/9784873117881

Reactビギナーズガイド : コンポーネントベースのフロントエンド開発入門

Stoyan Stefanov/牧野 聡 · オライリー・ジャパン · 2017年 · ¥2,750

FacebookのエンジニアによるReactの入門書! ReactによるコンポーネントベースのWebフロントエンド開発の…

詳細を tech-book.net で見る
tech-book.net /books/9784297129163

TypeScriptとReact/Next.jsでつくる実践Webアプリケーション開発

手島 拓也/吉田 健人/高林 佳稀 · 技術評論社 · 2022年

新しいフロントエンドの入門書決定版! 本書はReact/Next.jsとTypeScriptを用いてWebアプリケーションを開…

詳細を tech-book.net で見る
tech-book.net /books/9784844379546

【POD】React &amp; Gatsby開発入門

竹本 雄貴
詳細を tech-book.net で見る

全ガイドは ガイド一覧 から。連載は 連載一覧 へ。