Skip to content
Back to skills

Virtual Patterns

ASecurity

Auto-enforce TanStack Virtual best practices for list virtualization. Activates when implementing large lists, infinite scroll, virtualized grids, or performance optimization for long lists in React applications.

  • 6 stars
  • 0 votes
  • 0 copies
  • 1 view
  • Added May 28, 2026
ai-agentstypescriptreactnodeapiperformance

Works with

  • cli
  • api

Security analysis

A100/100

Scanned May 28, 2026

npx -y skills add smicolon/ai-kit --skill virtual-patterns --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Virtual Patterns?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Virtual Patterns
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/smicolon-virtual-patterns/badge)](https://www.skillsdirectory.com/skills/smicolon-virtual-patterns)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: TanStack Virtual Patterns
description: >-
  Auto-enforce TanStack Virtual best practices for list virtualization. Activates
  when implementing large lists, infinite scroll, virtualized grids, or performance
  optimization for long lists in React applications.
version: 1.0.0
---

# TanStack Virtual Patterns

This skill enforces TanStack Virtual best practices for efficient rendering of large lists.

## Basic Virtual List

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

interface VirtualListProps<T> {
  items: T[]
  renderItem: (item: T, index: number) => React.ReactNode
  estimateSize?: number
}

export function VirtualList<T>({
  items,
  renderItem,
  estimateSize = 50,
}: VirtualListProps<T>) {
  const parentRef = useRef<HTMLDivElement>(null)

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

  return (
    <div
      ref={parentRef}
      className="h-[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)`,
            }}
          >
            {renderItem(items[virtualItem.index], virtualItem.index)}
          </div>
        ))}
      </div>
    </div>
  )
}
```

## Dynamic Size Virtual List

```typescript
import { useVirtualizer } from '@tanstack/react-virtual'
import { useRef, useCallback } from 'react'

export function DynamicVirtualList({ items }: { items: Post[] }) {
  const parentRef = useRef<HTMLDivElement>(null)

  const virtualizer = useVirtualizer({
    count: items.length,
    getScrollElement: () => parentRef.current,
    estimateSize: () => 100, // Estimated height
    measureElement: (element) => element.getBoundingClientRect().height,
  })

  return (
    <div ref={parentRef} className="h-[600px] overflow-auto">
      <div
        style={{
          height: `${virtualizer.getTotalSize()}px`,
          position: 'relative',
        }}
      >
        {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)`,
            }}
          >
            <PostCard post={items[virtualItem.index]} />
          </div>
        ))}
      </div>
    </div>
  )
}
```

## Virtual Grid

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

interface VirtualGridProps<T> {
  items: T[]
  columns: number
  renderItem: (item: T, index: number) => React.ReactNode
  rowHeight?: number
}

export function VirtualGrid<T>({
  items,
  columns,
  renderItem,
  rowHeight = 200,
}: VirtualGridProps<T>) {
  const parentRef = useRef<HTMLDivElement>(null)
  const rowCount = Math.ceil(items.length / columns)

  const rowVirtualizer = useVirtualizer({
    count: rowCount,
    getScrollElement: () => parentRef.current,
    estimateSize: () => rowHeight,
    overscan: 2,
  })

  return (
    <div ref={parentRef} className="h-[600px] overflow-auto">
      <div
        style={{
          height: `${rowVirtualizer.getTotalSize()}px`,
          position: 'relative',
        }}
      >
        {rowVirtualizer.getVirtualItems().map((virtualRow) => {
          const startIndex = virtualRow.index * columns
          const rowItems = items.slice(startIndex, startIndex + columns)

          return (
            <div
              key={virtualRow.key}
              style={{
                position: 'absolute',
                top: 0,
                left: 0,
                width: '100%',
                height: `${virtualRow.size}px`,
                transform: `translateY(${virtualRow.start}px)`,
                display: 'grid',
                gridTemplateColumns: `repeat(${columns}, 1fr)`,
                gap: '1rem',
              }}
            >
              {rowItems.map((item, i) => (
                <div key={startIndex + i}>
                  {renderItem(item, startIndex + i)}
                </div>
              ))}
            </div>
          )
        })}
      </div>
    </div>
  )
}
```

## Infinite Scroll with Virtual List

```typescript
import { useVirtualizer } from '@tanstack/react-virtual'
import { useInfiniteQuery } from '@tanstack/react-query'
import { useRef, useEffect } from 'react'
import { queryKeys } from '@/lib/query-keys'

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

  const {
    data,
    fetchNextPage,
    hasNextPage,
    isFetchingNextPage,
  } = useInfiniteQuery({
    queryKey: queryKeys.posts.list({ infinite: true }),
    queryFn: ({ pageParam = 1 }) => postApi.getPosts({ page: pageParam }),
    getNextPageParam: (lastPage) =>
      lastPage.hasMore ? lastPage.nextPage : undefined,
    initialPageParam: 1,
  })

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

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

  const virtualItems = virtualizer.getVirtualItems()

  // Load more when reaching the end
  useEffect(() => {
    const lastItem = virtualItems[virtualItems.length - 1]
    if (!lastItem) return

    if (
      lastItem.index >= allItems.length - 1 &&
      hasNextPage &&
      !isFetchingNextPage
    ) {
      fetchNextPage()
    }
  }, [virtualItems, hasNextPage, isFetchingNextPage, allItems.length, fetchNextPage])

  return (
    <div ref={parentRef} className="h-[600px] overflow-auto">
      <div
        style={{
          height: `${virtualizer.getTotalSize()}px`,
          position: 'relative',
        }}
      >
        {virtualItems.map((virtualItem) => {
          const isLoader = virtualItem.index >= allItems.length

          return (
            <div
              key={virtualItem.key}
              style={{
                position: 'absolute',
                top: 0,
                left: 0,
                width: '100%',
                height: `${virtualItem.size}px`,
                transform: `translateY(${virtualItem.start}px)`,
              }}
            >
              {isLoader ? (
                <div className="flex items-center justify-center h-full">
                  Loading more...
                </div>
              ) : (
                <PostCard post={allItems[virtualItem.index]} />
              )}
            </div>
          )
        })}
      </div>
    </div>
  )
}
```

## Horizontal Virtual List

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

export function HorizontalVirtualList({ items }: { items: Image[] }) {
  const parentRef = useRef<HTMLDivElement>(null)

  const virtualizer = useVirtualizer({
    horizontal: true,
    count: items.length,
    getScrollElement: () => parentRef.current,
    estimateSize: () => 200,
    overscan: 3,
  })

  return (
    <div ref={parentRef} className="w-full overflow-x-auto">
      <div
        style={{
          width: `${virtualizer.getTotalSize()}px`,
          height: '200px',
          position: 'relative',
        }}
      >
        {virtualizer.getVirtualItems().map((virtualItem) => (
          <div
            key={virtualItem.key}
            style={{
              position: 'absolute',
              top: 0,
              left: 0,
              height: '100%',
              width: `${virtualItem.size}px`,
              transform: `translateX(${virtualItem.start}px)`,
            }}
          >
            <img
              src={items[virtualItem.index].url}
              alt={items[virtualItem.index].alt}
              className="h-full w-full object-cover"
            />
          </div>
        ))}
      </div>
    </div>
  )
}
```

## Virtual Table (with TanStack Table)

```typescript
import { useVirtualizer } from '@tanstack/react-virtual'
import { useReactTable, getCoreRowModel, flexRender } from '@tanstack/react-table'
import { useRef } from 'react'

export function VirtualTable<T>({
  data,
  columns,
}: {
  data: T[]
  columns: ColumnDef<T>[]
}) {
  const parentRef = useRef<HTMLDivElement>(null)

  const table = useReactTable({
    data,
    columns,
    getCoreRowModel: getCoreRowModel(),
  })

  const { rows } = table.getRowModel()

  const virtualizer = useVirtualizer({
    count: rows.length,
    getScrollElement: () => parentRef.current,
    estimateSize: () => 50,
    overscan: 10,
  })

  return (
    <div ref={parentRef} className="h-[600px] overflow-auto">
      <table className="w-full">
        <thead className="sticky top-0 bg-white z-10">
          {table.getHeaderGroups().map((headerGroup) => (
            <tr key={headerGroup.id}>
              {headerGroup.headers.map((header) => (
                <th key={header.id}>
                  {flexRender(header.column.columnDef.header, header.getContext())}
                </th>
              ))}
            </tr>
          ))}
        </thead>
        <tbody>
          <tr style={{ height: `${virtualizer.getTotalSize()}px` }}>
            <td colSpan={columns.length} style={{ padding: 0 }}>
              <div style={{ position: 'relative' }}>
                {virtualizer.getVirtualItems().map((virtualRow) => {
                  const row = rows[virtualRow.index]
                  return (
                    <div
                      key={row.id}
                      style={{
                        position: 'absolute',
                        top: 0,
                        left: 0,
                        width: '100%',
                        height: `${virtualRow.size}px`,
                        transform: `translateY(${virtualRow.start}px)`,
                        display: 'flex',
                      }}
                    >
                      {row.getVisibleCells().map((cell) => (
                        <div key={cell.id} className="flex-1 px-4 py-2">
                          {flexRender(cell.column.columnDef.cell, cell.getContext())}
                        </div>
                      ))}
                    </div>
                  )
                })}
              </div>
            </td>
          </tr>
        </tbody>
      </table>
    </div>
  )
}
```

## Scroll to Index

```typescript
export function ScrollableVirtualList({ items }: { items: Post[] }) {
  const parentRef = useRef<HTMLDivElement>(null)

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

  const scrollToItem = (index: number) => {
    virtualizer.scrollToIndex(index, { align: 'center', behavior: 'smooth' })
  }

  return (
    <div>
      <div className="flex gap-2 mb-4">
        <button onClick={() => scrollToItem(0)}>First</button>
        <button onClick={() => scrollToItem(Math.floor(items.length / 2))}>
          Middle
        </button>
        <button onClick={() => scrollToItem(items.length - 1)}>Last</button>
      </div>
      <div ref={parentRef} className="h-[400px] overflow-auto">
        {/* Virtual list content */}
      </div>
    </div>
  )
}
```

## Conventions to Enforce

1. **Ref for scroll element** - Always use `useRef` for parent container
2. **Fixed height container** - Parent must have defined height/overflow
3. **Overscan** - Include 3-5 items for smooth scrolling
4. **Absolute positioning** - Use transform for item placement
5. **Stable keys** - Use virtualItem.key, not index
6. **Dynamic sizing** - Use `measureElement` for variable heights
7. **Combine with Query** - Use infinite queries for data loading

## Anti-Patterns to Block

```typescript
// ❌ WRONG: No fixed height container
<div ref={parentRef}>
  {virtualItems.map(...)}
</div>

// ✅ CORRECT: Fixed height with overflow
<div ref={parentRef} className="h-[400px] overflow-auto">
  {virtualItems.map(...)}
</div>

// ❌ WRONG: Using index as key
{virtualItems.map((item, index) => (
  <div key={index}>...</div>
))}

// ✅ CORRECT: Using virtualItem.key
{virtualItems.map((virtualItem) => (
  <div key={virtualItem.key}>...</div>
))}

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

// ✅ CORRECT: Include overscan
const virtualizer = useVirtualizer({
  count: items.length,
  getScrollElement: () => parentRef.current,
  estimateSize: () => 50,
  overscan: 5,
})
```

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.