Skip to main content
Back to Blog
Tutorials
3 min read
November 24, 2024

How to Implement Virtual Scrolling with TanStack Virtual in React

Render huge lists efficiently with TanStack Virtual for smooth 60fps scrolling through thousands of items in React.

Ryel Banfield

Founder & Lead Developer

Virtual scrolling renders only visible items, enabling smooth scrolling through 100K+ rows.

Step 1: Install TanStack Virtual

pnpm add @tanstack/react-virtual

Step 2: Basic Virtual List

"use client";

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

const items = Array.from({ length: 50000 }, (_, i) => ({
  id: i,
  name: `Item ${i + 1}`,
  value: Math.floor(Math.random() * 1000),
}));

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

  const virtualizer = useVirtualizer({
    count: items.length,
    getScrollElement: () => parentRef.current,
    estimateSize: () => 50, // Estimated row height in px
    overscan: 5, // Extra items rendered outside viewport
  });

  return (
    <div ref={parentRef} className="h-[500px] overflow-auto rounded-xl border dark:border-gray-700">
      <div
        style={{
          height: `${virtualizer.getTotalSize()}px`,
          width: "100%",
          position: "relative",
        }}
      >
        {virtualizer.getVirtualItems().map((virtualItem) => {
          const item = items[virtualItem.index];
          return (
            <div
              key={virtualItem.key}
              style={{
                position: "absolute",
                top: 0,
                left: 0,
                width: "100%",
                height: `${virtualItem.size}px`,
                transform: `translateY(${virtualItem.start}px)`,
              }}
              className="flex items-center justify-between border-b px-4 dark:border-gray-800"
            >
              <span className="text-sm">{item.name}</span>
              <span className="text-sm text-gray-500">{item.value}</span>
            </div>
          );
        })}
      </div>
    </div>
  );
}

Step 3: Virtual Table

"use client";

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

interface Column<T> {
  key: keyof T;
  label: string;
  width: number;
}

interface VirtualTableProps<T> {
  data: T[];
  columns: Column<T>[];
  rowHeight?: number;
}

export function VirtualTable<T extends { id: string | number }>({
  data,
  columns,
  rowHeight = 40,
}: VirtualTableProps<T>) {
  const parentRef = useRef<HTMLDivElement>(null);

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

  const totalWidth = columns.reduce((sum, col) => sum + col.width, 0);

  return (
    <div className="rounded-xl border dark:border-gray-700">
      {/* Header */}
      <div
        className="flex border-b bg-gray-50 dark:border-gray-700 dark:bg-gray-800"
        style={{ width: totalWidth }}
      >
        {columns.map((col) => (
          <div
            key={String(col.key)}
            className="px-3 py-2 text-xs font-semibold uppercase tracking-wider text-gray-500"
            style={{ width: col.width }}
          >
            {col.label}
          </div>
        ))}
      </div>

      {/* Body */}
      <div
        ref={parentRef}
        className="overflow-auto"
        style={{ height: "500px" }}
      >
        <div
          style={{
            height: `${virtualizer.getTotalSize()}px`,
            width: totalWidth,
            position: "relative",
          }}
        >
          {virtualizer.getVirtualItems().map((virtualRow) => {
            const row = data[virtualRow.index];
            return (
              <div
                key={virtualRow.key}
                className="flex border-b hover:bg-gray-50 dark:border-gray-800 dark:hover:bg-gray-800/50"
                style={{
                  position: "absolute",
                  top: 0,
                  left: 0,
                  width: "100%",
                  height: `${virtualRow.size}px`,
                  transform: `translateY(${virtualRow.start}px)`,
                }}
              >
                {columns.map((col) => (
                  <div
                    key={String(col.key)}
                    className="flex items-center px-3 text-sm"
                    style={{ width: col.width }}
                  >
                    {String(row[col.key])}
                  </div>
                ))}
              </div>
            );
          })}
        </div>
      </div>

      {/* Footer */}
      <div className="border-t px-3 py-2 text-xs text-gray-500 dark:border-gray-700">
        {data.length.toLocaleString()} rows
      </div>
    </div>
  );
}

Step 4: Variable Height Items

"use client";

import { useRef, useCallback } from "react";
import { useVirtualizer } from "@tanstack/react-virtual";

const messages = Array.from({ length: 10000 }, (_, i) => ({
  id: i,
  text: "Lorem ipsum ".repeat(Math.floor(Math.random() * 10) + 1).trim(),
  author: `User ${Math.floor(Math.random() * 100)}`,
}));

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

  const virtualizer = useVirtualizer({
    count: messages.length,
    getScrollElement: () => parentRef.current,
    estimateSize: () => 80, // Rough estimate
    overscan: 5,
  });

  const measureElement = useCallback(
    (el: HTMLDivElement | null) => {
      if (!el) return;
      const index = Number(el.dataset.index);
      virtualizer.measureElement(el);
    },
    [virtualizer]
  );

  return (
    <div ref={parentRef} className="h-[500px] overflow-auto">
      <div
        style={{
          height: `${virtualizer.getTotalSize()}px`,
          position: "relative",
        }}
      >
        {virtualizer.getVirtualItems().map((virtualItem) => {
          const message = messages[virtualItem.index];
          return (
            <div
              key={virtualItem.key}
              data-index={virtualItem.index}
              ref={measureElement}
              style={{
                position: "absolute",
                top: 0,
                left: 0,
                width: "100%",
                transform: `translateY(${virtualItem.start}px)`,
              }}
              className="border-b p-3 dark:border-gray-800"
            >
              <p className="text-xs font-medium text-blue-600">
                {message.author}
              </p>
              <p className="mt-1 text-sm text-gray-600 dark:text-gray-400">
                {message.text}
              </p>
            </div>
          );
        })}
      </div>
    </div>
  );
}

Step 5: Virtual Grid

"use client";

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

export function VirtualGrid({ items }: { items: { id: number; src: string }[] }) {
  const parentRef = useRef<HTMLDivElement>(null);
  const columns = 4;
  const rowCount = Math.ceil(items.length / columns);

  const virtualizer = useVirtualizer({
    count: rowCount,
    getScrollElement: () => parentRef.current,
    estimateSize: () => 200,
    overscan: 2,
  });

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

          return (
            <div
              key={virtualRow.key}
              className="grid grid-cols-4 gap-2 p-1"
              style={{
                position: "absolute",
                top: 0,
                left: 0,
                width: "100%",
                height: `${virtualRow.size}px`,
                transform: `translateY(${virtualRow.start}px)`,
              }}
            >
              {rowItems.map((item) => (
                <div
                  key={item.id}
                  className="overflow-hidden rounded-lg border dark:border-gray-700"
                >
                  <img
                    src={item.src}
                    alt=""
                    className="h-full w-full object-cover"
                    loading="lazy"
                  />
                </div>
              ))}
            </div>
          );
        })}
      </div>
    </div>
  );
}

Need High-Performance Data UIs?

We build web applications optimized for large datasets with virtual scrolling, lazy loading, and fast data tables. Contact us to discuss your project.

virtual scrollingTanStack VirtualperformanceReacttutorial

Ready to Start Your Project?

RCB Software builds world-class websites and applications for businesses worldwide.

Get in Touch

Related Articles