Welcome back, intrepid developer! In our journey through the TanStack ecosystem, we’ve explored how to manage server state with Query, build dynamic interfaces with Table, and navigate complex applications with Router. But what happens when your beautifully crafted table suddenly needs to display thousands of rows, or your list component has tens of thousands of items? Performance can quickly grind to a halt, leading to sluggish UIs and frustrated users.
This chapter introduces you to TanStack Virtual, the headless superhero designed to tackle this exact problem. We’ll dive deep into the concept of UI virtualization, understand why it’s a game-changer for large datasets, and learn how to implement it step-by-step. By the end, you’ll be able to build lightning-fast lists and tables, even with massive amounts of data, ensuring a smooth and responsive user experience. Get ready to unlock a new level of frontend performance!
The Challenge of Large Datasets: Why Virtualization?
Imagine you have a list of 10,000 product items or a table with 5,000 user records. If you try to render all these items directly into the Document Object Model (DOM) at once, your browser will likely struggle. Here’s why:
- Memory Consumption: Each DOM element consumes memory. Thousands of elements can quickly exhaust available memory, especially on less powerful devices.
- Rendering Time: The browser has to calculate the layout, styles, and paint every single element. This process is computationally expensive and scales linearly with the number of elements.
- Reflows and Repaints: Interacting with the page (scrolling, resizing) or updating state can trigger expensive reflows (recalculating layout) and repaints (redrawing elements) for all visible elements, leading to jank and freezes.
Have you ever scrolled a page and felt it stutter or freeze momentarily? That’s often the symptom of rendering too many elements at once.
Virtualization is a technique that solves this by rendering only the items that are currently visible within the user’s viewport, plus a few extra items just outside the view for a smoother scrolling experience. As the user scrolls, the library dynamically swaps out the “virtual” items, recycling existing DOM elements and updating their content. This drastically reduces the number of elements the browser needs to manage at any given time.
Let’s visualize this concept:
As you can see, the TanStack Virtual Library acts as a smart intermediary, ensuring that only a manageable subset of your data ever makes it to the DOM. It’s like having a tiny, efficient window into a much larger world!
Core Concepts of TanStack Virtual
TanStack Virtual is a headless library, meaning it provides the logic for virtualization without dictating your UI. You have complete control over how your virtualized list or table looks. This is consistent with the TanStack philosophy we’ve seen in Query and Table.
The core of TanStack Virtual revolves around a hook (or function, depending on your framework adapter) that gives you information about which items should be rendered. For React, this is the useVirtualizer hook from @tanstack/react-virtual.
Let’s break down its key aspects:
1. The useVirtualizer Hook (for React)
This hook is your entry point to virtualization. It takes a configuration object and returns an object containing virtualized items and utility functions.
Key Configuration Options:
count: This is the total number of items in your list or table. It’s crucial for the virtualizer to know the full extent of the scrollable area.getScrollElement: A function that returns the DOM element responsible for scrolling. This is typically arefto the container element that you want to be scrollable.estimateSize: A function that estimates the size (height for vertical lists, width for horizontal) of each item. This is vital for the virtualizer to calculate scroll positions accurately. If your items have fixed sizes, this is straightforward. If they have dynamic sizes, you might need more advanced techniques or a good average estimate.overscan: The number of items to render outside the visible viewport. A higheroverscanvalue can make scrolling smoother by pre-rendering items before they fully enter view, but it also increases the number of DOM elements.horizontal: A boolean flag to indicate if you’re virtualizing horizontally instead of vertically.
2. virtualItems
The useVirtualizer hook returns an array of virtualItems. These are not your actual data items. Instead, each virtualItem is an object that tells you:
index: The index of the item from your originalcountthat should be rendered.start: The pixel offset from the top (or left for horizontal) of the scroll container where this item should be positioned.size: The calculated size (height/width) of this item.measureElement: A ref callback function to attach to the rendered DOM element. This allows the virtualizer to accurately measure the actual size of items, especially useful for dynamic heights.
You’ll use these virtualItems to render your actual data, positioning them with inline styles (transform: translateY(start)) to achieve the virtualized effect.
Step-by-Step Implementation: Virtualizing a Simple List
Let’s build a simple React component that displays a large list of numbers, first without virtualization to see the problem, then with TanStack Virtual.
First, let’s make sure we have the necessary packages. As of 2026-01-07, the latest stable version of @tanstack/react-virtual is v3.
# Install TanStack Virtual for React
npm install @tanstack/react-virtual@latest
# or
yarn add @tanstack/react-virtual@latest
1. The Problem: A Non-Virtualized List
Create a new React component, NonVirtualizedList.jsx:
// src/components/NonVirtualizedList.jsx
import React from 'react';
const NonVirtualizedList = ({ itemCount = 10000 }) => {
const items = Array.from({ length: itemCount }, (_, i) => `Item ${i}`);
return (
<div className="list-container" style={{ height: '400px', overflowY: 'scroll', border: '1px solid #ccc' }}>
<h3>Non-Virtualized List ({itemCount} items)</h3>
{items.map((item, index) => (
<div key={index} className="list-item" style={{ padding: '10px', borderBottom: '1px solid #eee' }}>
{item}
</div>
))}
</div>
);
};
export default NonVirtualizedList;
Now, let’s use it in our App.jsx (or a similar main component):
// src/App.jsx (or equivalent)
import React from 'react';
import NonVirtualizedList from './components/NonVirtualizedList';
import VirtualizedList from './components/VirtualizedList'; // We'll create this next
import './App.css'; // For basic styling
function App() {
return (
<div className="App">
<h1>TanStack Virtual Demo</h1>
<p>Observe the performance difference when scrolling.</p>
<div style={{ display: 'flex', gap: '20px' }}>
<NonVirtualizedList itemCount={10000} />
{/* We'll add VirtualizedList here later */}
</div>
</div>
);
}
export default App;
And some basic CSS in App.css:
/* src/App.css */
body {
font-family: sans-serif;
margin: 20px;
background-color: #f4f4f4;
}
.App {
text-align: center;
}
.list-container {
background-color: white;
margin: 0 auto;
width: 45%; /* Adjust for two lists side-by-side */
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.list-item {
box-sizing: border-box; /* Include padding in element's total width and height */
background-color: #f9f9f9;
}
.list-item:nth-child(even) {
background-color: #eef;
}
Run your application. Try scrolling the non-virtualized list with 10,000 items. You’ll likely notice a significant lag, especially if you scroll quickly. The browser’s developer tools (Performance tab) will show high CPU usage and long rendering times. This is the problem TanStack Virtual solves!
2. Virtualizing the List with useVirtualizer
Now, let’s create VirtualizedList.jsx and apply TanStack Virtual.
// src/components/VirtualizedList.jsx
import React, { useRef } from 'react';
// Import the useVirtualizer hook for React
import { useVirtualizer } from '@tanstack/react-virtual';
const VirtualizedList = ({ itemCount = 10000 }) => {
// 1. Create a ref for the scrollable container element
const parentRef = useRef(null);
// 2. Initialize the virtualizer hook
const rowVirtualizer = useVirtualizer({
count: itemCount, // Total number of items in our list
getScrollElement: () => parentRef.current, // Function to get the scrollable DOM element
estimateSize: () => 32, // Estimated height of each item (in pixels)
overscan: 5, // Render 5 items above and below the visible area for smooth scrolling
});
// These are our actual data items (can be fetched from an API, etc.)
const items = Array.from({ length: itemCount }, (_, i) => `Item ${i}`);
return (
<div
ref={parentRef} // Attach the ref to our scrollable container
className="list-container"
style={{
height: '400px',
overflowY: 'auto', // Important: Make it scrollable!
border: '1px solid #ccc',
}}
>
<h3>Virtualized List ({itemCount} items)</h3>
<div
style={{
height: rowVirtualizer.getTotalSize(), // Set the total height of the scrollable area
width: '100%',
position: 'relative', // Necessary for absolute positioning of virtual items
}}
>
{/* Map over the virtualizer's virtualItems, NOT your data items directly */}
{rowVirtualizer.getVirtualItems().map((virtualItem) => (
<div
key={virtualItem.index}
// Position each virtual item using transform for performance
style={{
position: 'absolute',
top: 0,
left: 0,
width: '100%',
height: `${virtualItem.size}px`, // Use the virtualizer's calculated size
transform: `translateY(${virtualItem.start}px)`, // Move item to its correct scroll position
padding: '10px',
borderBottom: '1px solid #eee',
boxSizing: 'border-box',
backgroundColor: virtualItem.index % 2 === 0 ? '#f9f9f9' : '#eef', // Apply alternating background
}}
>
{items[virtualItem.index]} {/* Render the actual data item */}
</div>
))}
</div>
</div>
);
};
export default VirtualizedList;
Explanation of the code added:
import { useVirtualizer } from '@tanstack/react-virtual';: We bring in the star of the show.const parentRef = useRef(null);: We create a React ref. This ref will be attached to thedivthat acts as our scrollable container. The virtualizer needs to know which element to observe for scrolling events.useVirtualizer(...):count: itemCount: We tell the virtualizer that our conceptual list hasitemCounttotal items.getScrollElement: () => parentRef.current: This function provides the actual DOM element that the virtualizer will monitor for scrolling.estimateSize: () => 32: We’re estimating that each item is 32 pixels tall. This is crucial for the virtualizer to calculate the total scrollable height and item positions. If your items have fixed heights, this should match exactly.overscan: 5: This meansTanStack Virtualwill render 5 items above and 5 items below the currently visible range. This helps prevent flicker during fast scrolling.
ref={parentRef}: We attach our ref to the outerdivthat will be the scroll container.overflowY: 'auto': This CSS property is essential! It makes theparentRefelement scrollable, allowing the virtualizer to detect scroll events.height: rowVirtualizer.getTotalSize(): The innerdiv(which holds the virtual items) is given a height equal to the total estimated height of allitemCountitems. This creates the conceptual scrollable area, making the scrollbar appear correctly.position: 'relative': This is needed because we’re going to absolutely position the individual virtual items within this container.rowVirtualizer.getVirtualItems().map(...): Instead of mapping overitemsdirectly, we map over the array returned bygetVirtualItems(). This array contains only thevirtualItemobjects that are currently within the viewport (plus overscan).key={virtualItem.index}: It’s important to use theindexfromvirtualItemas the key, as this uniquely identifies the original data item.position: 'absolute',top: 0,left: 0,transform: translateY(${virtualItem.start}px): This is the magic! Each virtual item is absolutely positioned within therelativeparent. ThetransformYCSS property is used to move the item to its correct vertical position within the scrollable area.transformis generally more performant thantopfor animations and positioning because it doesn’t trigger layout recalculations for other elements.height:${virtualItem.size}px``: We use thesizeprovided by the virtualizer for the item’s height. This is especially useful if you have dynamic heights andTanStack Virtualhas measured them.items[virtualItem.index]: Finally, inside the virtualizeddiv, we render the actual data corresponding tovirtualItem.index.
Now, update your App.jsx to include VirtualizedList:
// src/App.jsx (updated)
import React from 'react';
import NonVirtualizedList from './components/NonVirtualizedList';
import VirtualizedList from './components/VirtualizedList';
import './App.css';
function App() {
return (
<div className="App">
<h1>TanStack Virtual Demo</h1>
<p>Observe the performance difference when scrolling.</p>
<div style={{ display: 'flex', justifyContent: 'space-around', gap: '20px' }}>
<NonVirtualizedList itemCount={10000} />
<VirtualizedList itemCount={10000} />
</div>
</div>
);
}
export default App;
Refresh your browser. You should now see two lists. Scroll both and compare the performance. The virtualized list should scroll incredibly smoothly, even with 10,000 items, while the non-virtualized one will likely still be sluggish. This is the power of virtualization!
Mini-Challenge: Dynamic Row Heights
Our current VirtualizedList assumes a fixed estimateSize of 32px. What if your list items have variable heights (e.g., text content of varying length)?
Challenge: Modify the VirtualizedList component to handle dynamic row heights.
Hint:
useVirtualizerprovides ameasureElementproperty on eachvirtualItem. This is arefcallback that you should attach to the actual DOM element of your item.- When
measureElementis attached,TanStack Virtualwill automatically measure the item’s actual size and update its internal calculations. - You might need to adjust your
estimateSizeto be a function that returns an initial estimate, and then letmeasureElementrefine it.
Try to implement this before looking at a potential solution!
Click for a Hint/Solution approach
For dynamic heights, TanStack Virtual needs to know the actual height of each rendered item. You provide this by attaching the measureElement ref callback from virtualItem to the root DOM element of your rendered item.
// Inside VirtualizedList.jsx, modify the map function:
{rowVirtualizer.getVirtualItems().map((virtualItem) => (
<div
key={virtualItem.index}
ref={rowVirtualizer.measureElement} // Attach the virtualizer's measureElement ref here
// ... other styles ...
style={{
// ... existing styles ...
// Make sure the height is not fixed by inline style here if it's truly dynamic.
// The virtualizer will manage the transformY.
height: 'auto', // Allow content to dictate height initially, virtualizer will measure
// ... existing transformY style ...
}}
>
{items[virtualItem.index]}
{/* Add some dynamic content to make height vary */}
<p style={{ fontSize: `${10 + (virtualItem.index % 10)}px` }}>
This item has dynamic content and height! {Array(virtualItem.index % 5).fill('more text ').join('')}
</p>
</div>
))}
You’ll also want to make sure your estimateSize is a reasonable average, but the measureElement is the key for accurate dynamic sizing.
Integrating with TanStack Table
Virtualizing with TanStack Table is a powerful combination. TanStack Table handles all the complex table logic (sorting, filtering, grouping), and TanStack Virtual ensures that only the visible rows are rendered.
The integration is straightforward:
TanStack Tableprovides therows: Your table instance will give you an array ofrows(after sorting, filtering, etc.)TanStack Virtualusesrows.lengthforcount: You pass therows.lengthtouseVirtualizer.TanStack Virtualmanages row rendering: You iterate overrowVirtualizer.getVirtualItems()and usetable.getRow(virtualItem.index)to get the correctTanStack Tablerow object.
Here’s a conceptual snippet for integrating with TanStack Table:
// Conceptual snippet: Not a full runnable example, focuses on integration points
import React, { useRef } from 'react';
import { useReactTable, getCoreRowModel, flexRender } from '@tanstack/react-table';
import { useVirtualizer } from '@tanstack/react-virtual';
// Assume 'data' and 'columns' are defined elsewhere
// const data = ...;
// const columns = ...;
const VirtualizedTable = ({ data, columns }) => {
const table = useReactTable({
data,
columns,
getCoreRowModel: getCoreRowModel(),
// ... other TanStack Table options (sorting, filtering, etc.)
});
const { rows } = table.getRowModel(); // Get the processed rows from TanStack Table
const parentRef = useRef(null);
const rowVirtualizer = useVirtualizer({
count: rows.length, // Use the total number of processed rows from TanStack Table
getScrollElement: () => parentRef.current,
estimateSize: () => 40, // Estimate row height
overscan: 10,
});
const virtualRows = rowVirtualizer.getVirtualItems();
return (
<div
ref={parentRef}
className="table-container"
style={{ height: '500px', overflowY: 'auto', border: '1px solid #ccc' }}
>
<table>
<thead>
{table.getHeaderGroups().map((headerGroup) => (
<tr key={headerGroup.id}>
{headerGroup.headers.map((header) => (
<th key={header.id} colSpan={header.colSpan}>
{header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())}
</th>
))}
</tr>
))}
</thead>
<tbody
style={{
height: rowVirtualizer.getTotalSize(), // Total height of the scrollable tbody
position: 'relative',
}}
>
{virtualRows.map((virtualRow) => {
const row = rows[virtualRow.index]; // Get the actual TanStack Table row object
return (
<tr
key={row.id}
data-index={virtualRow.index}
ref={rowVirtualizer.measureElement} // For dynamic row heights
style={{
position: 'absolute',
top: 0,
left: 0,
width: '100%',
height: `${virtualRow.size}px`,
transform: `translateY(${virtualRow.start}px)`,
}}
>
{row.getVisibleCells().map((cell) => (
<td key={cell.id}>{flexRender(cell.column.columnDef.cell, cell.getContext())}</td>
))}
</tr>
);
})}
</tbody>
</table>
</div>
);
};
This pattern is extremely powerful, allowing you to build feature-rich and performant data grids for enterprise applications.
Common Pitfalls & Troubleshooting
- Incorrect
getScrollElement:- Symptom: The virtualized list doesn’t scroll, or the scrollbar doesn’t appear/work correctly.
- Cause: You haven’t correctly identified the DOM element that is actually scrolling. It must be an element with
overflow: autooroverflow: scroll. - Fix: Double-check that
parentRef.current(or whatever you’re returning fromgetScrollElement) points to the correct scrollable container. Ensure that container has anoverflowstyle applied.
- Inaccurate
estimateSize:- Symptom: Content jumps or flickers during scrolling, or the scrollbar length doesn’t accurately reflect the total list size.
- Cause: Your
estimateSizefunction is providing a value significantly different from the actual item heights. - Fix: For fixed-height items, ensure
estimateSizematches the exact height. For dynamic items, provide a good average estimate and always userowVirtualizer.measureElementon your rendered items to allow the virtualizer to correct itself.
- Missing
position: relativeorposition: absolute:- Symptom: Items overlap, appear in the wrong order, or are not positioned correctly.
- Cause: The CSS required for
transformYpositioning isn’t set up. - Fix: Ensure your immediate parent of the virtualized items has
position: relativeand each virtual item hasposition: absolute,top: 0,left: 0, and thetransform: translateY(...)style.
- Performance issues within virtualized items:
- Symptom: Scrolling is smooth, but individual items are slow to render or update.
- Cause: The problem isn’t the number of items, but the complexity or inefficient rendering inside each item component.
- Fix: Optimize the components rendered for each
virtualItem. UseReact.memofor components that don’t need to re-render often, avoid unnecessary calculations, and profile individual item components. Remember, virtualization only helps with the number of elements, not the complexity of those elements.
Summary
Congratulations! You’ve successfully navigated the world of UI virtualization with TanStack Virtual.
Here are the key takeaways from this chapter:
- Virtualization is essential for maintaining performance and a smooth user experience when dealing with large lists or tables (hundreds to thousands of items).
- TanStack Virtual is a headless library that provides the core logic for virtualization, giving you full control over your UI.
- The
useVirtualizerhook (for React) is your primary tool, requiringcount,getScrollElement, andestimateSizeto function. - You map over
rowVirtualizer.getVirtualItems()and usetransform: translateY()to position your actual data items efficiently. - Integrating
TanStack VirtualwithTanStack Tablecreates highly performant data grids. - Common pitfalls include incorrect scroll element references, inaccurate size estimates, and missing CSS positioning.
By leveraging TanStack Virtual, you can build applications that remain snappy and responsive, no matter how much data you throw at them. This is a critical skill for modern frontend development!
In the next chapter, we’ll explore TanStack Store, a flexible and reactive state management library that underpins much of the TanStack ecosystem, and how it can be used for client-side state alongside TanStack Query’s server-state management.
References
- TanStack Virtual Official Documentation
- TanStack React Virtual Documentation
- Building a Performant Virtualized Table with @tanstack/react-table and @tanstack/react-virtual
This page is AI-assisted and reviewed. It references official documentation and recognized resources where relevant.