React Virtual
The @tanstack/react-virtual adapter is a wrapper around the core virtual logic.
useVirtualizer#
function useVirtualizer<TScrollElement, TItemElement = unknown>(
options: PartialKeys<
ReactVirtualizerOptions<TScrollElement, TItemElement>,
'observeElementRect' | 'observeElementOffset' | 'scrollToFn'
>,
): Virtualizer<TScrollElement, TItemElement>This function returns a standard Virtualizer instance configured to work with an HTML element as the scrollElement.
useWindowVirtualizer#
function useWindowVirtualizer<TItemElement = unknown>(
options: PartialKeys<
ReactVirtualizerOptions<Window, TItemElement>,
| 'getScrollElement'
| 'observeElementRect'
| 'observeElementOffset'
| 'scrollToFn'
>,
): Virtualizer<Window, TItemElement>This function returns a window-based Virtualizer instance configured to work with the window as the scrollElement.
React-Specific Options#
useFlushSync#
type ReactVirtualizerOptions<TScrollElement, TItemElement> =
VirtualizerOptions<TScrollElement, TItemElement> & {
useFlushSync?: boolean
}Both useVirtualizer and useWindowVirtualizer accept a useFlushSync option that controls whether React's flushSync is used for synchronous updates.
- Type: boolean
- Default: true
- Description: When true, the virtualizer will use flushSync from react-dom to ensure synchronous rendering during scroll events. This provides the most accurate scrolling behavior but may impact performance in some scenarios.
When to disable useFlushSync#
You may want to set useFlushSync: false in the following scenarios:
- React 19 compatibility: In React 19, you may see the following console warning when scrolling:Setting useFlushSync: false will eliminate this warning by allowing React to batch updates naturally.plaintext
flushSync was called from inside a lifecycle method. React cannot flush when React is already rendering. Consider moving this call to a scheduler task or micro task. - Performance optimization: If you experience performance issues with rapid scrolling on lower-end devices
- Testing environments: When running tests that don't require synchronous DOM updates
- Non-critical lists: When slight visual delays during scrolling are acceptable for better overall performance
Example#
const virtualizer = useVirtualizer({
count: 10000,
getScrollElement: () => parentRef.current,
estimateSize: () => 50,
useFlushSync: false, // Disable synchronous updates
})directDomUpdates#
- Type: boolean
- Default: false
- Description: Skip React re-renders for scroll-only updates. When enabled, the virtualizer writes item positions (top/left or transform) and the container size (height/width) directly to the DOM, and only re-renders when the visible index range or isScrolling changes.
Requirements when enabled#
- Item elements must be position: absolute; in 'transform' mode they must also be anchored with top: 0 / left: 0.
- Item elements must not set the main-axis position in their style — the virtualizer owns top / left in 'position' mode and transform in 'transform' mode.
- The inner size container must receive virtualizer.containerRef and must not set height / width in its style.
- For multi-lane layouts (grids / masonry), the cross-axis position (e.g. left: ${(item.lane * 100) / lanes}%) is stable per item and must still be set in your JSX — only the main axis is automated.
⚠️ This flag is intended to be set once at mount. Toggling it (or directDomUpdatesMode) at runtime can leave stale inline styles on items and the container.
Note: If you omit containerRef, the virtualizer makes no direct DOM writes — it writes neither item positions nor the container size. You're then responsible for positioning items and sizing the container yourself (e.g. in onChange), while still benefiting from the skipped re-renders.
Example#
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>
)directDomUpdatesMode#
- Type: 'position' | 'transform'
- Default: 'transform'
- Description: Controls how directDomUpdates positions item elements.
- 'transform' (default): writes transform: translate3d(...). Promotes items to their own compositor layer — usually smoother on long lists, but creates a stacking context and can interfere with position: fixed descendants. Item elements must be anchored with position: absolute, top: 0, and left: 0.
- 'position': writes top / left. Item elements must be position: absolute.
Example#
const virtualizer = useVirtualizer({
count: 10000,
getScrollElement: () => parentRef.current,
estimateSize: () => 50,
directDomUpdates: true,
directDomUpdatesMode: 'position', // Use top/left instead of transform
})