К основному содержанию
скилл CX Codex

tanstack-virtual — скилл для Claude Code: виртуализация списков

Скилл подсказывает агенту, как подключить TanStack Virtual и рисовать только видимые строки в больших списках, таблицах и сетках.

Установить в Claude Code · Через Skills CLI (Vercel) Все способы и платформы ↓
npx skills add tanstack-skills/tanstack-skills --skill tanstack-virtual -a claude-code
Открыть на GitHub

Что делает tanstack-virtual

Скилл описывает работу с @tanstack/react-virtual и ядром @tanstack/virtual-core. Виртуализатор считает, какие элементы попали в область прокрутки, и расставляет их абсолютным позиционированием. Число узлов DOM остаётся небольшим при любом размере данных.

Внутри — разбор базового паттерна useVirtualizer: обязательные count, getScrollElement, estimateSize и список необязательных опций вроде overscan, lanes, getItemKey. Отдельно разобраны переменная высота через measureElement с атрибутом data-index, горизонтальная прокрутка, сетка из двух виртуализаторов и useWindowVirtualizer для прокрутки окна. Пакет ставится через npm install @tanstack/react-virtual.

Для динамических высот estimateSize советует завышать оценку, иначе строки будут прыгать при измерении. Ещё один момент: при measureElement нельзя задавать фиксированную высоту элемента — размер должен определять контент.

Какие задачи решает

  • отрисовать список на 10000 строк без просадки кадров
  • собрать виртуализированную сетку из строк и столбцов
  • подключить переменную высоту строк через измерение элементов
  • сделать горизонтальную прокрутку длинной ленты карточек
  • перевести прокрутку на окно браузера вместо внутреннего контейнера

Примеры запросов агенту

  • Помоги подключить useVirtualizer для списка из 10000 строк
  • Сделай виртуализированную сетку с горизонтальной прокруткой на TanStack Virtual
  • У меня строки разной высоты — настрой measureElement и data-index

Как установить tanstack-virtual

1Через Skills CLI (Vercel)

npx skills add tanstack-skills/tanstack-skills --skill tanstack-virtual -a claude-code

2Вручную: положить файлы

~/.claude/skills/tanstack-virtual/SKILL.md

Папка скилла целиком; для одного проекта — .claude/skills/tanstack-virtual/

По шагам, с командами для каждой платформы: Как установить скилл в Claude Code: пять способов

Вопросы о tanstack-virtual

Как установить tanstack-virtual в Claude?

Выполните npx skills add tanstack-skills/tanstack-skills --skill tanstack-virtual -a claude-code. Файл скилла окажется по пути ~/.claude/skills/tanstack-virtual/SKILL.md.

Что делает tanstack-virtual?

Даёт агенту готовые шаблоны виртуализации на TanStack Virtual: базовый список, переменные высоты, горизонтальную прокрутку, сетку и прокрутку окна.

Какие опции обязательны у useVirtualizer?

count, getScrollElement и estimateSize. Остальные — необязательные, у overscan значение по умолчанию 1.

Исходный текст

SKILL.md · MIT
---
name: tanstack-virtual
description: Headless UI for virtualizing large element lists at 60FPS in TS/JS, React, Vue, Solid, Svelte, Lit & Angular.
---


## Overview

TanStack Virtual provides virtualization logic for rendering only visible items in large lists, grids, and tables. It calculates which items are in the viewport and positions them with absolute positioning, keeping DOM node count minimal regardless of dataset size.

**Package:** `@tanstack/react-virtual`
**Core:** `@tanstack/virtual-core` (framework-agnostic)

## Installation

```bash
npm install @tanstack/react-virtual
```

## Core Pattern

```tsx
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 Options

### Required

| Option | Type | Description |
|--------|------|-------------|
| `count` | `number` | Total number of items |
| `getScrollElement` | `() => Element \| null` | Returns scroll container |
| `estimateSize` | `(index) => number` | Estimated item size (overestimate recommended) |

### Optional

| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `overscan` | `number` | `1` | Extra items rendered beyond viewport |
| `horizontal` | `boolean` | `false` | Horizontal virtualization |
| `gap` | `number` | `0` | Gap between items (px) |
| `lanes` | `number` | `1` | Number of lanes (masonry/grid) |
| `paddingStart` | `number` | `0` | Padding before first item |
| `paddingEnd` | `number` | `0` | Padding after last item |
| `scrollPaddingStart` | `number` | `0` | Offset for scrollTo positioning |
| `scrollPaddingEnd` | `number` | `0` | Offset for scrollTo positioning |
| `initialOffset` | `number` | `0` | Starting scroll position |
| `initialRect` | `Rect` | - | Initial dimensions (SSR) |
| `enabled` | `boolean` | `true` | Enable/disable |
| `getItemKey` | `(index) => Key` | `(i) => i` | Stable key for items |
| `rangeExtractor` | `(range) => number[]` | default | Custom visible indices |
| `scrollToFn` | `(offset, options, instance) => void` | default | Custom scroll behavior |
| `measureElement` | `(el, entry, instance) => number` | default | Custom measurement |
| `onChange` | `(instance, sync) => void` | - | State change callback |
| `isScrollingResetDelay` | `number` | `150` | Delay before scroll complete |

## Virtualizer API

```typescript
// Get visible items
virtualizer.getVirtualItems(): VirtualItem[]

// Get total scrollable size
virtualizer.getTotalSize(): number

// Scroll to specific index
virtualizer.scrollToIndex(index, { align: 'start' | 'center' | 'end' | 'auto', behavior: 'auto' | 'smooth' })

// Scroll to offset
virtualizer.scrollToOffset(offset, options)

// Force recalculation
virtualizer.measure()
```

## VirtualItem Properties

```typescript
interface VirtualItem {
  key: Key           // Unique key
  index: number      // Index in source data
  start: number      // Pixel offset (use for transform)
  end: number        // End pixel offset
  size: number       // Item dimension
  lane: number       // Lane index (multi-column)
}
```

## Dynamic/Variable Heights

Use `measureElement` ref for items with unknown heights:

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

{virtualizer.getVirtualItems().map((virtualItem) => (
  <div
    key={virtualItem.key}
    data-index={virtualItem.index}  // REQUIRED for measurement
    ref={virtualizer.measureElement} // Attach for dynamic measurement
    style={{
      position: 'absolute',
      top: 0,
      left: 0,
      width: '100%',
      transform: `translateY(${virtualItem.start}px)`,
      // Do NOT set fixed height - let content determine it
    }}
  >
    {items[virtualItem.index].content}
  </div>
))}
```

## Horizontal Virtualization

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

// Use width for container, translateX for positioning
<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>
```

## Grid Virtualization (Two Virtualizers)

```tsx
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>
  )
}
```

## Window Scrolling

```tsx
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>
  )
}
```

## Infinite Scrolling

```tsx
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])

  // Render virtual items, show loader row for last item if loading
}
```

## Sticky Items

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

const stickyIndexes = [0, 10, 20, 30] // Header indices

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]),
})

// Render sticky items with position: sticky; top: 0; zIndex: 1
```

## Smooth Scrolling

```tsx
const virtualizer = useVirtualizer({
  scrollToFn: (offset, { behavior }, instance) => {
    if (behavior === 'smooth') {
      // Custom easing animation
      instance.scrollElement?.scrollTo({ top: offset, behavior: 'smooth' })
    } else {
      instance.scrollElement?.scrollTo({ top: offset })
    }
  },
})

// Usage
virtualizer.scrollToIndex(500, { align: 'center', behavior: 'smooth' })
```

## Best Practices

1. **Overestimate `estimateSize`** - prevents scroll jumps (items shrinking causes issues)
2. **Increase `overscan`** (3-5) to reduce blank flashing during fast scrolling
3. **Use `transform: translateY()`** over `top` for GPU-composited positioning
4. **Add `data-index` attribute** when using `measureElement` for dynamic sizing
5. **Don't set fixed height** on dynamically measured items
6. **Use `getItemKey`** for stable keys when items can reorder
7. **Use `gap` option** instead of margins (margins interfere with measurement)
8. **Use `paddingStart/End`** instead of CSS padding on the container
9. **Use `enabled: false`** to pause when the list is hidden
10. **Memoize callbacks** (`estimateSize`, `getItemKey`, `rangeExtractor`)
11. **Use `will-change: transform`** CSS on items for GPU acceleration

## Common Pitfalls

- Setting fixed height on dynamically measured items
- Using CSS margins instead of the `gap` option
- Forgetting `data-index` with `measureElement`
- Not providing `position: relative` on the inner container
- Underestimating `estimateSize` (causes scroll jumps)
- Setting `overscan` too low for fast scrolling (blank items)
- Forgetting to subtract `scrollMargin` from `translateY` in window scrolling
- Not memoizing the `estimateSize` function (causes re-renders)

© tanstack-skills · MIT · tanstack-skills/tanstack-skills

Похожие скиллы

Всё по теме «Фронтенд и UI» →
virtual-lists patterns.dev Обучает агента технике виртуализации списков и таблиц в веб-приложениях. Помогает убрать задержки при скролле и ускорить начальную отрисовку страниц. CX Codex +1 1 500установок clerk-tanstack-patterns clerk Подсказывает агенту, как подключить Clerk к TanStack React Start: защита маршрутов, createServerFn, лоадеры и middleware. CX Codex +1 15 500установок tanstack-router-best-practices DeckardGer Набор правил по TanStack Router для React: типизация, загрузка данных, search params и навигация. Пригодится, когда роутинг в проекте разрастается. CX Codex +1 7 100установок tanstack-query Mindrally Скилл задаёт агенту правила работы с серверным состоянием в React: ключи запросов, queryOptions, мутации и переход с v4 на v5. CX Codex +1 724установок tanstack-router PatrickJS Правило для Cursor, которое задаёт агенту стандарты TanStack Router v1: файловые маршруты, типизация параметров и загрузка данных через loader. ★ 41 011звёзд репо frontend-dev-guidelines sickn33 Задаёт строгие правила для React и TypeScript: структура фич, Suspense-запросы, MUI и проверка влияния на производительность. CX Codex +1 1 500установок

Из того же набора12

tanstack-router Скилл по TanStack Router для React и Solid: файловый роутинг, поисковые параметры и загрузка данных с проверкой типов. CX Codex +1 4 900установок tanstack-form Скилл учит ассистента создавать формы на базе TanStack Form с типизацией TypeScript, валидацией через Zod или Valibot и динамическими массивами полей. CX Codex +1 4 300установок tanstack-pacer Скилл подсказывает агенту, как подключить TanStack Pacer: дебаунс, троттлинг, лимиты запросов, очереди и батчинг в одном пакете. CX Codex +1 3 900установок tanstack-start Скилл помогает создавать и сопровождать приложения на фреймворке TanStack Start с потоковым SSR, серверными функциями и маршрутизацией. CX Codex +1 3 900установок tanstack-devtools Скилл учит агента подключать единую панель отладки TanStack в проектах на React. Помогает настроить плагины для Query, Router и рабочих процессов TanStack AI. CX Codex +1 2 300установок tanstack-store Скилл по TanStack Store: реактивный стор без привязки к фреймворку, адаптеры для React, Vue, Solid, Angular и Svelte. Пригодится, когда агент пишет состояние на @tanstack/store. CX Codex +1 1 800установок tanstack-cli Скилл проводит агента через создание проекта TanStack Start: подбирает интеграции, генерирует структуру и подсказывает по деплою. CX Codex +1 1 700установок tanstack-ai Скилл обучает ассистента работать с TanStack AI: настраивать стриминг ответов, вызов функций и валидацию схем в React или Solid. CX Codex +1 1 600установок

Спонсоры проекта

Компании, которые помогают держать аналитику открытой