typescriptadvanced

React Virtual List Component

Render large lists efficiently with windowing to only mount visible items in the viewport.

typescript
'use client';
import { useState, useRef, useCallback, useEffect } from 'react';

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

export function VirtualList<T>({
  items,
  itemHeight,
  containerHeight,
  overscan = 5,
  renderItem,
}: VirtualListProps<T>) {
  const [scrollTop, setScrollTop] = useState(0);
  const containerRef = useRef<HTMLDivElement>(null);

  const totalHeight = items.length * itemHeight;
  const startIndex = Math.max(0, Math.floor(scrollTop / itemHeight) - overscan);
  const endIndex = Math.min(
    items.length,
    Math.ceil((scrollTop + containerHeight) / itemHeight) + overscan
  );

  const visibleItems = items.slice(startIndex, endIndex);

  const onScroll = useCallback((e: React.UIEvent<HTMLDivElement>) => {
    setScrollTop(e.currentTarget.scrollTop);
  }, []);

  return (
    <div
      ref={containerRef}
      onScroll={onScroll}
      style={{ height: containerHeight, overflow: 'auto' }}
    >
      <div style={{ height: totalHeight, position: 'relative' }}>
        {visibleItems.map((item, i) => (
          <div
            key={startIndex + i}
            style={{
              position: 'absolute',
              top: (startIndex + i) * itemHeight,
              height: itemHeight,
              width: '100%',
            }}
          >
            {renderItem(item, startIndex + i)}
          </div>
        ))}
      </div>
    </div>
  );
}

// Usage
interface LogEntry { id: number; message: string; level: string }

function LogViewer({ logs }: { logs: LogEntry[] }) {
  return (
    <VirtualList
      items={logs}
      itemHeight={40}
      containerHeight={600}
      renderItem={(log, index) => (
        <div className="flex items-center px-4 border-b border-white/5">
          <span className="text-gray-500 w-12">{index}</span>
          <span className={log.level === 'error' ? 'text-red-400' : 'text-gray-300'}>
            {log.message}
          </span>
        </div>
      )}
    />
  );
}

Use Cases

  • Rendering thousands of list items efficiently
  • Log viewers and data tables without lag
  • Infinite scroll with constant DOM node count

Tags

Related Snippets

Similar patterns you can reuse in the same workflow.