tanstack-virtual

📁 fellipeutaka/leon 📅 8 days ago
4
总安装量
4
周安装量
#52356
全站排名
安装命令
npx skills add https://github.com/fellipeutaka/leon --skill tanstack-virtual

Agent 安装分布

opencode 4
gemini-cli 4
antigravity 4
claude-code 4
github-copilot 4
codex 4

Skill 文档

TanStack Virtual (React)

Installation

npm install @tanstack/react-virtual

Core Concept

Three required options — everything else is optional:

count          — total item count
getScrollElement — () => scrollableElement
estimateSize   — (index) => number (px)

You must:

  1. Create a full-height container using getTotalSize()
  2. Absolutely position each virtual item using virtualItem.start

Fixed Size (Vertical)

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

function List({ items }: { items: string[] }) {
  const parentRef = useRef<HTMLDivElement>(null)

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

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

Dynamic Size (measured at runtime)

const virtualizer = useVirtualizer({
  count: items.length,
  getScrollElement: () => parentRef.current,
  estimateSize: () => 100, // err large for better scroll-to-index accuracy
})

{virtualizer.getVirtualItems().map((item) => (
  <div
    key={item.key}
    data-index={item.index}          // required for measureElement
    ref={virtualizer.measureElement} // measures on mount/resize
    style={{
      position: 'absolute',
      top: 0,
      left: 0,
      width: '100%',
      transform: `translateY(${item.start}px)`,
      // Do NOT set height — virtualizer controls it
    }}
  >
    {items[item.index]}
  </div>
))}

Window Virtualizer

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

function WindowList({ items }: { items: string[] }) {
  const listRef = useRef<HTMLDivElement>(null)

  const virtualizer = useWindowVirtualizer({
    count: items.length,
    estimateSize: () => 35,
    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',
              top: 0,
              left: 0,
              width: '100%',
              height: `${item.size}px`,
              transform: `translateY(${item.start - virtualizer.options.scrollMargin}px)`,
            }}
          >
            {items[item.index]}
          </div>
        ))}
      </div>
    </div>
  )
}

scrollMargin = distance from page top to list top. Subtract it from item.start in the transform.

Imperative Scroll

virtualizer.scrollToIndex(50, { align: 'start' }) // 'start' | 'center' | 'end' | 'auto'
virtualizer.scrollToOffset(1200, { behavior: 'smooth' })

Critical Rules

Always:

  • overflow: auto on scroll container
  • Explicit height/width on scroll container
  • position: relative on inner container sized to getTotalSize()
  • position: absolute on each virtual item
  • translateY/translateX for positioning (not top/left)
  • data-index on items when using measureElement
  • item.key as React key (not item.index)
  • scrollMargin for window virtualizers when content precedes the list

Never:

  • Set height on dynamically measured items
  • Omit overflow: auto on scroll container
  • Use smooth scroll behavior with dynamic-size items (documented limitation)

Key Options

Option Default Purpose
overscan 1 Extra items rendered outside visible area
gap 0 Spacing between items (px) — use instead of CSS margin
paddingStart 0 Padding before first item (px)
paddingEnd 0 Padding after last item (px)
horizontal false Horizontal orientation
lanes 1 Columns (vertical) or rows (horizontal) for masonry
getItemKey index Stable item keys — always set when items have IDs
enabled true Set false to disable observers and reset state
isRtl false RTL horizontal scroll

References