with-tanstack-virtual

SkillAI & models

Virtualize final React Table row or column models with TanStack Virtual. Load for useVirtualizer counts, scroll elements, stable keys, data-index measurement, dynamic heights, sticky headers/columns, grid/flex geometry, or infinite fetching; Virtual is renderer composition, not a Table feature.

Available today. Use it from your connected AI after setup.

Add ahel to your AI once: Claude, ChatGPT, Cursor, Claude Code or Codex. Then ask it to use this.

Then ask your AI: use the with-tanstack-virtual skill

What this skill tells your AI

The instructions your AI receives, as published by octanejs/octane in packages/tanstack-table/upstream/skills/with-tanstack-virtual/SKILL.md and read by ahel’s review.

This skill builds on @tanstack/table-core#core, getting-started, and table-state. Build the Table model first, then virtualize its final rendered rows or visible columns.

Setup

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

function VirtualBody({ table }: { table: any }) {
  const scrollRef = useRef<HTMLDivElement>(null)
  const rows = table.getRowModel().rows
  const virtualizer = useVirtualizer({
    count: rows.length,
    getScrollElement: () => scrollRef.current,
    estimateSize: () => 36,
    getItemKey: (index) => rows[index].id,
    overscan: 5,
  })
  return (
    <div ref={scrollRef} style={{ height: 400, overflow: 'auto' }}>
      <div style={{ height: virtualizer.getTotalSize(), position: 'relative' }}>
        {virtualizer.getVirtualItems().map((item) => (
          <div
            key={rows[item.index].id}
            data-index={item.index}
            ref={virtualizer.measureElement}
            style={{
              position: 'absolute',
              transform: `translateY(${item.start}px)`,
              width: '100%',
            }}
          >
            {rows[item.index].getAllCells().map((cell: any) => (
              <span
                key={cell.id}
                style={{
                  display: 'inline-block',
                  width: cell.column.getSize(),
                }}
              >
                <table.FlexRender cell={cell} />
              </span>
            ))}
          </div>
        ))}
      </div>
    </div>
  )
}

Core Patterns

Keep the virtualizer near its render loop

const rows = table.getRowModel().rows
const rowVirtualizer = useVirtualizer({
  count: rows.length,
  getScrollElement: () => container.current,
  estimateSize: () => 36,
})

This limits unrelated parent updates and keeps count, measurement, and rendered items together.

Use Table sizes in renderer CSS

<td style={{ width: cell.column.getSize() }}>
  <table.FlexRender cell={cell} />
</td>

Table calculates size state; the renderer must apply it.

Common Mistakes

HIGH Registering Virtual as a feature

Wrong:

const features = tableFeatures({ rowVirtualizer: useVirtualizer(options) })

Correct:

const rows = table.getRowModel().rows
const virtualizer = useVirtualizer({
  count: rows.length,
  getScrollElement: () => scrollRef.current,
  estimateSize: () => 36,
})

Virtual controls rendering geometry and is not a Table feature or row model.

Source: docs/framework/react/guide/virtualization.md

HIGH Virtualizing raw input data

Wrong:

const virtualizer = useVirtualizer({
  count: data.length,
  getScrollElement,
  estimateSize,
})

Correct:

const rows = table.getRowModel().rows
const virtualizer = useVirtualizer({
  count: rows.length,
  getScrollElement,
  estimateSize,
})

Raw data ignores filtering, sorting, expansion, grouping, and pagination already applied by Table.

Source: examples/react/virtualized-rows

HIGH Measuring against incomplete identity

Wrong:

<tr ref={virtualizer.measureElement}>{row.id}</tr>

Correct:

<tr key={row.id} data-index={item.index} ref={virtualizer.measureElement}>
  {row.id}
</tr>

Dynamic measurement needs the virtual index, and stable row identity prevents measurements moving to the wrong row.

Source: examples/react/virtualized-rows

HIGH Omitting positioning geometry

Wrong:

<tbody>{virtualizer.getVirtualItems().map(renderRow)}</tbody>

Correct:

<tbody
  style={{
    display: 'grid',
    height: virtualizer.getTotalSize(),
    position: 'relative',
  }}
>
  {virtualizer.getVirtualItems().map(renderRow)}
</tbody>

Virtual only computes positions; the renderer must provide total spacer size and position items using each virtual start.

Source: examples/react/virtualized-rows

API Discovery

Inspect node_modules/@tanstack/react-table/dist/index.d.ts for Table render APIs and installed node_modules/@tanstack/react-virtual/dist/ for the exact virtualizer options. Copy layout contracts from the maintained example matching rows, columns, or infinite loading.

Signals

GitHub stars
1k
Forks
55
Last commit
Sep 2026
Advanced
Item type
skill
Key
with-tanstack-virtual
Source
github.com/octanejs/octane