TanStack Virtual

repository·main·Indexed 11 days ago

https://github.com/TanStack/virtual

A headless, framework-agnostic virtualization library used to render large datasets, including lists, grids, and tables, efficiently at 60FPS.

Tokens
40.7K
Snippets
147
Records
176
Agent score
91%

What's inside TanStack Virtual

  1. What is TanStack Virtual

    main

    TanStack Virtual is a headless, framework-agnostic virtualization library designed to render massive lists, grids, and tables at 60FPS. Because it is headless, it provides the logic for virtualization while giving you full control over your own markup and styles.

    Key features include:

    • Framework-agnostic: Works across different UI frameworks.
    • Versatile Layouts: Supports vertical, horizontal, and grid layouts using a single hook or function.
    • Lightweight: Small footprint (approximately 10–15kb).
    • Dynamic Sizing: Supports both dynamic and measured sizing for items.
    • High Performance: Optimized for smooth 60FPS scrolling, including support for sticky items and window-scrolling utilities.
  2. Explore the TanStack Ecosystem

    main

    TanStack Virtual is part of a larger ecosystem of high-quality libraries. You can explore related tools such as:

    • TanStack Config: Tooling for JS/TS packages
    • TanStack DB: Reactive sync client store
    • TanStack DevTools: Unified devtools panel
    • TanStack Form: Type-safe form state
    • TanStack Pacer: Debouncing, throttling, and batching
    • TanStack Query: Async state and caching
    • TanStack Ranger: Range and slider primitives
    • TanStack Router: Type-safe routing, caching, and URL state
    • TanStack Start: Full-stack SSR and streaming
    • TanStack Store: Reactive data store
    • TanStack Table: Headless datagrids
  3. How the Virtualizer works

    main

    The core abstraction in this library is the Virtualizer.

    Key concepts:

    • Axes: Virtualizers can be oriented on a vertical axis (the default) or a horizontal axis. By combining both axis configurations, you can achieve grid-like virtualization.
    • Headless Implementation: The virtualizer manages the logic of which items should be visible and where they should be positioned, but it does not render them. You must use the virtualizer's API to render the items and apply the necessary styles (like transform or top/left) to position them correctly within a scrollable container.
  4. Enable `directDomUpdates` for high-performance scrolling

    main

    Setting directDomUpdates: true allows the virtualizer to skip React re-renders for scroll-only updates. Instead of waiting for a React render cycle, the virtualizer writes item positions and container dimensions directly to the DOM.

    Requirements for directDomUpdates:

    • Item Elements: Must have position: absolute. If using directDomUpdatesMode: 'transform', they must also be anchored with top: 0 and left: 0.
    • Item Styles: You must not set the main-axis position (top/left or transform) in your JSX; the virtualizer manages this.
    • Container: The inner size container must receive virtualizer.containerRef and must not have height or width set in its style.
    • Multi-lane layouts: For grids/masonry, you must still manually set the cross-axis position (e.g., left) in your JSX.

    ⚠️ Warning: This flag should be set once at mount. Toggling it at runtime can leave stale inline styles on items and the container.

    Note: If you omit containerRef, the virtualizer will not perform direct DOM writes for the container size or item positions, but you still benefit from skipped re-renders.

    const virtualizer = useVirtualizer({
      count: 10000,
      getScrollElement: () => parentRef.current,
      estimateSize: () => 50,
      directDomUpdates: true,
    })
    
    return (
      <div ref={parentRef} style={{ overflow: 'auto', height: 400 }}>
        {/* The inner container must use virtualizer.containerRef and not set height */}
        <div ref={virtualizer.containerRef} style={{ position: 'relative' }}>
          {virtualizer.getVirtualItems().map((item) => (
            <div
              key={item.key}
              ref={virtualizer.measureElement}
              data-index={item.index}
              style={{
                position: 'absolute',
                top: 0,
                left: 0,
                width: '100%',
                // Do NOT set top/left/transform — the virtualizer handles it
              }}
            >
              Row {item.index}
            </div>
          ))}
        </div>
      </div>
    )
  5. How Marko Virtual tags and variables work

    main

    The Marko adapter provides two auto-discovered, self-closing tags: <virtualizer> for element-based scrolling and <window-virtualizer> for full-page scrolling.

    Both tags expose a tag variable using the syntax <tagname/v/>. You use this variable to access the virtualization state (like v.virtualItems and v.totalSize) to render your visible content. You are responsible for the markup and positioning of the items.

  6. Handle masonry layout lane assignments

    main

    When using the lanes option for masonry layouts, you can control how items are assigned to lanes using laneAssignmentMode:

    • 'estimate' (Default): Assignments are cached immediately based on estimateSize. This prevents items from jumping between lanes during scrolling but may result in suboptimal lane usage if estimates are inaccurate.
    • 'measured': Lane caching is deferred until items are actually measured via measureElement. This results in more accurate layouts but may cause items to shift once they are first measured.

    Set lanes to the desired number of columns/rows.

  7. Manage the virtualizer's lifetime and scope

    main

    To ensure the virtualizer correctly tracks its scroll element, you must declare the <virtualizer> tag within the same conditional scope as its scroll element.

    If the scroll container is subject to unmounting or remounting (e.g., inside an <if> block or during a route transition), the <virtualizer> must be inside that same block. If the virtualizer is placed outside the conditional, it will remain mounted but bound to a removed element, causing it to render nothing when the element remounts. Co-locating them ensures that unmounting the element also tears down the virtualizer instance, and remounting builds a fresh one.

    <if=show>
      <virtualizer/v count=1000 estimateSize=() => 35 getScrollElement=() => scrollEl()/>
      <div/scrollEl class="list">...</div>
    </if>
  8. Keep older-history prepends stable with stable keys

    main

    When loading older messages and prepending them to your data array, TanStack Virtual can maintain the user's visual scroll position if you use stable keys.

    Crucial: Do not use array indices as keys. When you prepend items, every existing item's index changes, which breaks the virtualizer's ability to track them. Instead, use a unique identifier from your data (e.g., a message ID) in the getItemKey option.

    // Correct: Use a unique ID
    getItemKey: (index) => messages[index]!.id
    
    // Incorrect: Do not use index for chat history
    // getItemKey: (index) => index
  9. Understand the structure of the Marko browser test app

    main

    The test harness uses a single application architecture to manage multiple testing scenarios efficiently.

    Route Types

    • Option gates: These are artificial pages created to test specific configuration options. Each page corresponds to one option (e.g., enabled, rtl, scroll-margin, cached, lanes-mode, measure-element, debug, scroll-events, window-horizontal, window-initial-offset, window-example). They assert that enabling an option observably changes the virtualization behavior.
    • Example fixtures: These are thin wrappers that render real examples from the examples/marko/ directory. This ensures that the e2e suite tests the actual shipped examples without code duplication.

    Implementation Detail

    Example fixtures import the page component directly:

    import Page from "../../../../../../../examples/marko/fixed/src/routes/+page.marko"
    <Page/>
  10. Use Pretext for text-based row height estimation

    main

    TanStack Virtual handles scrolling and positioning, but for rows where height is primarily determined by wrapped text (e.g., chat logs, AI streams, comments), you can use Pretext to estimate heights without relying on expensive DOM measurements.

    When to use Pretext

    Use Pretext when a row's height can be derived from:

    • Text content
    • The exact canvas font string used in rendering
    • Available content width
    • Rendered line-height
    • Matching whitespace, word-break, and letter-spacing settings

    When NOT to use Pretext

    Do not use Pretext for rows containing:

    • Images or embeds
    • Block markdown
    • Loaded components
    • Arbitrary CSS layouts

    For these cases, continue using measureElement or call resizeItem once the content resolves.

  11. End-anchored virtualization for chat UIs

    main

    The Angular Chat example demonstrates how to implement virtualization specifically for chat-style user interfaces. It solves several common chat UX challenges using end-anchored virtualization:

    • Initial Position: The list starts at the newest message.
    • History Prepending: Keeps the currently visible messages stable when older message history is prepended to the list.
    • Auto-scroll Behavior: Automatically follows appended messages (new incoming messages) only if the user is already scrolled to the end.
    • Dynamic Content Stability: Remains pinned to the correct position while a dynamically measured reply (such as a streaming AI response) is being rendered.