Agent Skills by ALSEL
Anthropic Claudeソフトウェア開発⭐ リポ 0品質スコア 50/100

tanstack-virtual

TS/JS、React、Vue、Solid、Svelte、Lit、Angularに対応し、大量の要素リストを60FPSで仮想化するためのヘッドレスUIライブラリです。スクロールパフォーマンスが求められる大規模リストやテーブルの実装時に活用できます。

description の原文を見る

Headless UI for virtualizing large element lists at 60FPS in TS/JS, React, Vue, Solid, Svelte, Lit & Angular.

SKILL.md 本文

概要

TanStack Virtual は、大規模なリスト、グリッド、テーブル内で見えている要素のみをレンダリングするための仮想化ロジックを提供します。ビューポート内にある要素を計算し、絶対位置で配置することで、データセットのサイズに関わらず DOM ノード数を最小限に保ちます。

パッケージ: @tanstack/react-virtual コア: @tanstack/virtual-core (フレームワーク非依存)

インストール

npm install @tanstack/react-virtual

コア パターン

import { useVirtualizer } from '@tanstack/react-virtual'

function VirtualList() {
  const parentRef = useRef<HTMLDivElement>(null)

  const virtualizer = useVirtualizer({
    count: 10000,
    getScrollElement: () => parentRef.current,
    estimateSize: () => 35, // estimated row height in px
    overscan: 5,
  })

  return (
    <div ref={parentRef} style={{ height: '400px', overflow: 'auto' }}>
      <div
        style={{
          height: `${virtualizer.getTotalSize()}px`,
          width: '100%',
          position: 'relative',
        }}
      >
        {virtualizer.getVirtualItems().map((virtualItem) => (
          <div
            key={virtualItem.key}
            style={{
              position: 'absolute',
              top: 0,
              left: 0,
              width: '100%',
              height: `${virtualItem.size}px`,
              transform: `translateY(${virtualItem.start}px)`,
            }}
          >
            Row {virtualItem.index}
          </div>
        ))}
      </div>
    </div>
  )
}

Virtualizer オプション

必須

オプション説明
countnumber要素の総数
getScrollElement() => Element | nullスクロール コンテナを返す
estimateSize(index) => number推定される要素サイズ (多めに見積もることを推奨)

オプション

オプションデフォルト説明
overscannumber1ビューポート外でレンダリングする追加要素
horizontalbooleanfalse水平方向の仮想化
gapnumber0要素間のギャップ (px)
lanesnumber1レーン数 (マソンリー/グリッド)
paddingStartnumber0最初の要素前のパディング
paddingEndnumber0最後の要素後のパディング
scrollPaddingStartnumber0scrollTo 配置のオフセット
scrollPaddingEndnumber0scrollTo 配置のオフセット
initialOffsetnumber0開始スクロール位置
initialRectRect-初期寸法 (SSR)
enabledbooleantrue有効/無効
getItemKey(index) => Key(i) => i要素の安定したキー
rangeExtractor(range) => number[]デフォルトカスタム表示インデックス
scrollToFn(offset, options, instance) => voidデフォルトカスタム スクロール動作
measureElement(el, entry, instance) => numberデフォルトカスタム計測
onChange(instance, sync) => void-状態変化コールバック
isScrollingResetDelaynumber150スクロール完了までの遅延

Virtualizer API

// 表示されている要素を取得
virtualizer.getVirtualItems(): VirtualItem[]

// スクロール可能な総サイズを取得
virtualizer.getTotalSize(): number

// 特定のインデックスにスクロール
virtualizer.scrollToIndex(index, { align: 'start' | 'center' | 'end' | 'auto', behavior: 'auto' | 'smooth' })

// オフセットにスクロール
virtualizer.scrollToOffset(offset, options)

// 再計算を強制
virtualizer.measure()

VirtualItem プロパティ

interface VirtualItem {
  key: Key           // ユニークキー
  index: number      // ソース データ内のインデックス
  start: number      // ピクセル オフセット (transform に使用)
  end: number        // 終了ピクセル オフセット
  size: number       // 要素の寸法
  lane: number       // レーン インデックス (マルチ カラム)
}

動的/可変高さ

未知の高さを持つ要素に measureElement ref を使用します:

const virtualizer = useVirtualizer({
  count: items.length,
  getScrollElement: () => parentRef.current,
  estimateSize: () => 50, // overestimate
})

{virtualizer.getVirtualItems().map((virtualItem) => (
  <div
    key={virtualItem.key}
    data-index={virtualItem.index}  // 計測のために必須
    ref={virtualizer.measureElement} // 動的計測用にアタッチ
    style={{
      position: 'absolute',
      top: 0,
      left: 0,
      width: '100%',
      transform: `translateY(${virtualItem.start}px)`,
      // 固定高さを設定しないでください - コンテンツが決定させます
    }}
  >
    {items[virtualItem.index].content}
  </div>
))}

水平方向仮想化

const virtualizer = useVirtualizer({
  count: columns.length,
  getScrollElement: () => parentRef.current,
  estimateSize: () => 100,
  horizontal: true,
})

// コンテナの幅に translateX を使用して配置
<div style={{ width: `${virtualizer.getTotalSize()}px`, position: 'relative' }}>
  {virtualizer.getVirtualItems().map((item) => (
    <div style={{
      position: 'absolute',
      height: '100%',
      width: `${item.size}px`,
      transform: `translateX(${item.start}px)`,
    }}>
      Column {item.index}
    </div>
  ))}
</div>

グリッド仮想化 (2 つの Virtualizer)

function VirtualGrid() {
  const parentRef = useRef<HTMLDivElement>(null)

  const rowVirtualizer = useVirtualizer({
    count: 10000,
    getScrollElement: () => parentRef.current,
    estimateSize: () => 35,
    overscan: 5,
  })

  const columnVirtualizer = useVirtualizer({
    count: 10000,
    getScrollElement: () => parentRef.current,
    estimateSize: () => 100,
    horizontal: true,
    overscan: 5,
  })

  return (
    <div ref={parentRef} style={{ height: '500px', width: '500px', overflow: 'auto' }}>
      <div style={{
        height: `${rowVirtualizer.getTotalSize()}px`,
        width: `${columnVirtualizer.getTotalSize()}px`,
        position: 'relative',
      }}>
        {rowVirtualizer.getVirtualItems().map((virtualRow) => (
          <Fragment key={virtualRow.key}>
            {columnVirtualizer.getVirtualItems().map((virtualColumn) => (
              <div
                key={virtualColumn.key}
                style={{
                  position: 'absolute',
                  width: `${virtualColumn.size}px`,
                  height: `${virtualRow.size}px`,
                  transform: `translateX(${virtualColumn.start}px) translateY(${virtualRow.start}px)`,
                }}
              >
                Cell {virtualRow.index},{virtualColumn.index}
              </div>
            ))}
          </Fragment>
        ))}
      </div>
    </div>
  )
}

ウィンドウ スクロール

import { useWindowVirtualizer } from '@tanstack/react-virtual'

function WindowList() {
  const listRef = useRef<HTMLDivElement>(null)

  const virtualizer = useWindowVirtualizer({
    count: 10000,
    estimateSize: () => 45,
    overscan: 5,
    scrollMargin: listRef.current?.offsetTop ?? 0,
  })

  return (
    <div ref={listRef}>
      <div style={{
        height: `${virtualizer.getTotalSize()}px`,
        position: 'relative',
      }}>
        {virtualizer.getVirtualItems().map((item) => (
          <div
            key={item.key}
            style={{
              position: 'absolute',
              height: `${item.size}px`,
              transform: `translateY(${item.start - virtualizer.options.scrollMargin}px)`,
            }}
          >
            Row {item.index}
          </div>
        ))}
      </div>
    </div>
  )
}

無限スクロール

import { useVirtualizer } from '@tanstack/react-virtual'
import { useInfiniteQuery } from '@tanstack/react-query'

function InfiniteList() {
  const { data, fetchNextPage, hasNextPage, isFetchingNextPage } = useInfiniteQuery({
    queryKey: ['items'],
    queryFn: ({ pageParam = 0 }) => fetchItems(pageParam),
    getNextPageParam: (lastPage) => lastPage.nextCursor,
  })

  const allItems = data?.pages.flatMap((page) => page.items) ?? []

  const virtualizer = useVirtualizer({
    count: hasNextPage ? allItems.length + 1 : allItems.length,
    getScrollElement: () => parentRef.current,
    estimateSize: () => 50,
    overscan: 5,
  })

  useEffect(() => {
    const items = virtualizer.getVirtualItems()
    const lastItem = items[items.length - 1]
    if (lastItem && lastItem.index >= allItems.length - 1 && hasNextPage && !isFetchingNextPage) {
      fetchNextPage()
    }
  }, [virtualizer.getVirtualItems(), hasNextPage, isFetchingNextPage, allItems.length])

  // 仮想要素をレンダリング、読み込み中の場合は最後の要素に読み込みローダーを表示
}

スティッキー要素

import { defaultRangeExtractor, Range } from '@tanstack/react-virtual'

const stickyIndexes = [0, 10, 20, 30] // ヘッダー インデックス

const virtualizer = useVirtualizer({
  count: 1000,
  getScrollElement: () => parentRef.current,
  estimateSize: () => 50,
  rangeExtractor: useCallback((range: Range) => {
    const next = new Set([...stickyIndexes, ...defaultRangeExtractor(range)])
    return [...next].sort((a, b) => a - b)
  }, [stickyIndexes]),
})

// スティッキー要素を position: sticky; top: 0; zIndex: 1 でレンダリング

スムーズ スクロール

const virtualizer = useVirtualizer({
  scrollToFn: (offset, { behavior }, instance) => {
    if (behavior === 'smooth') {
      // カスタム イージング アニメーション
      instance.scrollElement?.scrollTo({ top: offset, behavior: 'smooth' })
    } else {
      instance.scrollElement?.scrollTo({ top: offset })
    }
  },
})

// 使用方法
virtualizer.scrollToIndex(500, { align: 'center', behavior: 'smooth' })

ベスト プラクティス

  1. estimateSize を多めに見積もる - スクロール ジャンプを防止 (要素が縮小するとトラブル)
  2. overscan を増やす (3-5) - 高速スクロール時の空白フラッシュを削減
  3. top の代わりに transform: translateY() を使用 - GPU コンポジット配置に
  4. 動的サイズ変更で measureElement を使用する場合は data-index 属性を追加
  5. 動的に計測された要素に固定高さを設定しない
  6. 要素が並び替わる可能性がある場合は getItemKey を使用 安定したキーのために
  7. CSS マージンの代わりに gap オプションを使用 (マージンは計測に干渉)
  8. コンテナ上の CSS パディングの代わりに paddingStart/End を使用
  9. リストが非表示の場合は enabled: false を使用 一時停止に
  10. コールバックをメモ化 (estimateSize, getItemKey, rangeExtractor)
  11. 要素上で will-change: transform CSS を使用 GPU アクセラレーション用

よくある落とし穴

  • 動的に計測される要素に固定高さを設定
  • CSS マージンの代わりに gap オプションを使用していない
  • measureElementdata-index を忘れる
  • 内側のコンテナに position: relative を指定していない
  • estimateSize を少なめに見積もる (スクロール ジャンプの原因)
  • overscan を低く設定して高速スクロール (空白要素が発生)
  • ウィンドウ スクロール時に translateY から scrollMargin を差し引くのを忘れる
  • estimateSize 関数をメモ化していない (再レンダリングの原因)

ライセンス: MIT(寛容ライセンスのため全文を引用しています) · 原本リポジトリ

詳細情報

作者
tanstack-skills
リポジトリ
tanstack-skills/tanstack-skills
ライセンス
MIT
最終更新
不明

Source: https://github.com/tanstack-skills/tanstack-skills / ライセンス: MIT

関連スキル

汎用ソフトウェア開発⭐ リポ 39,967

doubt-driven-development

重要な判断はすべて、本番環境への展開前に新しい視点から対抗的レビューを実施します。速度より正確性が重要な場合、不慣れなコードを扱う場合、本番環境・セキュリティに関わるロジック・取り消し不可の操作など影響度が高い場合、または後でバグを修正するよりも今検証する方が効率的な場合に活用してください。

by addyosmani
汎用ソフトウェア開発⭐ リポ 1,175

apprun-skills

TypeScriptを使用したAppRunアプリケーションのMVU設計に関する総合的なガイダンスが得られます。コンポーネントパターン、イベントハンドリング、状態管理(非同期ジェネレータを含む)、パラメータと保護機能を備えたルーティング・ナビゲーション、vistestを使用したテストに対応しています。AppRunコンポーネントの設計・レビュー、ルートの配線、状態フローの管理、AppRunテストの作成時に活用してください。

by yysun
OpenAIソフトウェア開発⭐ リポ 797

desloppify

コードベースのヘルスチェックと技術負債の追跡ツールです。コード品質、技術負債、デッドコード、大規模ファイル、ゴッドクラス、重複関数、コードスメル、命名規則の問題、インポートサイクル、結合度の問題についてユーザーが質問した場合に使用してください。また、ヘルススコアの確認、次の改善項目の提案、クリーンアップ計画の作成をリクエストされた際にも対応します。29言語に対応しています。

by Git-on-my-level
汎用ソフトウェア開発⭐ リポ 39,967

debugging-and-error-recovery

テストが失敗したり、ビルドが壊れたり、動作が期待と異なったり、予期しないエラーが発生したりした場合に、体系的な根本原因デバッグをガイドします。推測ではなく、根本原因を見つけて修正するための体系的なアプローチが必要な場合に使用してください。

by addyosmani
汎用ソフトウェア開発⭐ リポ 39,967

test-driven-development

テスト駆動開発により実装を進めます。ロジックの実装、バグの修正、動作の変更など、あらゆる場面で活用できます。コードが正常に動作することを証明する必要がある場合、バグ報告を受けた場合、既存機能を修正する予定がある場合に使用してください。

by addyosmani
汎用ソフトウェア開発⭐ リポ 39,967

incremental-implementation

変更を段階的に実施します。複数のファイルに影響する機能や変更を実装する場合に使用してください。大量のコードを一度に書こうとしている場合や、タスクが一度では完結できないほど大きい場合に活用します。

by addyosmani
本サイトは GitHub 上で公開されているオープンソースの SKILL.md ファイルをクロール・インデックス化したものです。 各スキルの著作権は原作者に帰属します。掲載に問題がある場合は info@alsel.co.jp または /takedown フォームよりご連絡ください。
原作者: tanstack-skills · tanstack-skills/tanstack-skills · ライセンス: MIT