TrueSpec

Infinite Scroll with Virtualization

TanStack Virtual + Intersection Observer for smooth infinite lists

What You’ll Build

After following this guide, you will have a working implementation of infinite scroll with virtualization in your project. Render lists of 10,000+ items without lag using virtualization. Only renders items currently visible in the viewport (typically 20-30 items), regardless of total list size. Combined with infinite scroll, this loads more data as the user scrolls without pagination buttons.

Use Cases & Problems Solved

  • Implement interactive UI features that users expect from modern apps
  • Follow established patterns that scale and remain maintainable
  • Reduce boilerplate and avoid common frontend pitfalls

Prerequisites

  • React 18+

Step-by-Step Implementation

Install TanStack Virtual + React Query

The following snippet shows how to install tanstack virtual + react query. Copy this into your project and adjust the values for your environment.

npm install @tanstack/react-virtual @tanstack/react-query

Infinite scroll with virtualization

The following snippet shows how to infinite scroll with virtualization. Copy this into your project and adjust the values for your environment.

import { useRef, useEffect } from 'react';
import { useInfiniteQuery } from '@tanstack/react-query';
import { useVirtualizer } from '@tanstack/react-virtual';

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

  const { data, fetchNextPage, hasNextPage, isFetchingNextPage } = useInfiniteQuery({
    queryKey: ['items'],
    queryFn: ({ pageParam = 0 }) =>
      fetch(\`/api/items?offset=\${pageParam}&limit=50\`).then(r => r.json()),
    getNextPageParam: (lastPage, allPages) =>
      lastPage.length === 50 ? allPages.length * 50 : undefined,
    initialPageParam: 0,
  });

  const allItems = data?.pages.flat() ?? [];

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

  useEffect(() => {
    const lastItem = virtualizer.getVirtualItems().at(-1);
    if (lastItem && lastItem.index >= allItems.length - 1 && hasNextPage && !isFetchingNextPage) {
      fetchNextPage();
    }
  }, [virtualizer.getVirtualItems(), hasNextPage, isFetchingNextPage]);

  return (
    <div ref={parentRef} style={{ height: '600px', overflow: 'auto' }}>
      <div style={{ height: virtualizer.getTotalSize(), position: 'relative' }}>
        {virtualizer.getVirtualItems().map(virtualRow => (
          <div key={virtualRow.key}
            style={{ position: 'absolute', top: virtualRow.start, height: virtualRow.size, width: '100%' }}>
            {allItems[virtualRow.index]?.name ?? 'Loading...'}
          </div>
        ))}
      </div>
    </div>
  );
}

⚠️ Don’t Do This

❌ Rendering all items in a large list

// Renders ALL 10,000 items — browser freezes!
{items.map(item => <div key={item.id}>{item.name}</div>)}

✅ Use virtualization — only render visible items (~20-30)

// Only renders items in the viewport!
const virtualizer = useVirtualizer({
  count: items.length,
  getScrollElement: () => parentRef.current,
  estimateSize: () => 60,
});

Testing

Verify your implementation with these tests:

// __tests__/infinite-scroll-with-virtualization.test.ts
import { describe, it, expect } from 'vitest';

describe('Infinite Scroll with Virtualization', () => {
  it('should initialize without errors', () => {
    // Test that the setup completes successfully
    expect(() => setup()).not.toThrow();
  });

  it('should handle the primary use case', async () => {
    const result = await execute();
    expect(result).toBeDefined();
    expect(result.success).toBe(true);
  });

  it('should handle edge cases', async () => {
    // Test with empty/null input
    const result = await execute(null);
    expect(result.error).toBeDefined();
  });
});

Verification

# Load page with 10,000+ items
# Open Performance tab in DevTools
# Scroll rapidly — should stay at 60fps
# Check DOM node count — should stay under 100 even with 10K items

Related Specs

Intermediate

Next.js Server Actions Guide

Form mutations, revalidation, optimistic updates, and error handling

Frontend Patterns
Beginner

Toast Notification System

Custom toast component, queue management, animations, and accessibility

Frontend Patterns
Beginner

React Form Validation (React Hook Form + Zod)

Type-safe forms, custom validators, error display, and submission handling

Frontend Patterns