mrkeyoor.com_
Sun 20 Sept 11:42 UTC
npmWeb Frontendupdated 20 Sept 2026

@tanstack/react-virtual review

@tanstack/react-virtual 3.14.10 is the React adapter for TanStack's headless list and grid virtualizer. useVirtualizer computes which items overlap a scroll viewport and returns keys, dimensions, offsets, and total scroll size. Your component still owns the scroll box, spacer, absolute positioning, row markup, accessibility, measurement refs, and loading behavior. Its single direct dependency is virtual-core 3.17.8, whose current fixes reset stuck scrolling state during cleanup and ignore connected measurement nodes outside the item count. Our full-package browser build measured 11.5 KB gzipped.

Verdict

@tanstack/react-virtual 3.14.10 installed in 2.2 seconds and measured 11.5 KB gzipped in our browser build, with 0 audit findings. Choose it when a large React list truly needs virtualization and your team wants headless control; a finished list component is cheaper when you do not want to own CSS, accessibility, and measurement behavior.

We installed it

Lab card: what happened when we installed @tanstack/react-virtualScreenshot of @tanstack/react-virtual documentation
Install✓ · 2.2s5 packages on disk · 9 MB
ImportESM import works · require() works · ESM package with exports map
Browser11.5 KBgzipped (36.4 KB minified), bundled with esbuild
TypesTypeScript types bundled
Known vulns00 critical · 0 high · 0 moderate · 0 low (npm audit)

Answers from our run

Does @tanstack/react-virtual install cleanly?

Yes. In a fresh container with an empty cache, npm install @tanstack/react-virtual finished in 2 seconds, leaving 5 packages and 9 MB on disk. npm audit reported no known vulnerabilities.

How much does @tanstack/react-virtual add to a browser bundle?

11.5 KB gzipped (36.4 KB minified) when the whole package is bundled for the browser with esbuild. Importing only part of it is usually smaller.

Does @tanstack/react-virtual work with both ESM and CommonJS?

Yes. Both import '@tanstack/react-virtual' and require('@tanstack/react-virtual') worked in Node 22 in our run. The package is published as ESM with an exports map.

Does @tanstack/react-virtual include TypeScript types?

Yes, type declarations ship inside the package, so no @types install is needed.

@tanstack/react-virtual or react-virtuoso: which should you use?

react-virtuoso: Use it when React 19 screens need a component-level list with grouped and chat behavior. @tanstack/react-virtual 3.14.10 installed in 2.2 seconds and measured 11.5 KB gzipped in our browser build, with 0 audit findings.

When should you not use @tanstack/react-virtual?

The list has a few hundred cheap rows; virtualization adds measurement and scroll-restoration bugs before it saves useful work

API stability4/5Version 3.14.10 still centers on useVirtualizer, useWindowVirtualizer, getVirtualItems, getTotalSize, measureElement, and scroll methods. New capabilities usually arrive as core options instead of replacement React components. The adapter and @tanstack/virtual-core use different version numbers, with this adapter depending on core 3.17.8, so consumers must inspect both changelogs when scrolling or measurement behavior changes.
Docs4/5TanStack documents virtualizer options and methods, with working examples for fixed lists, dynamic rows, tables, grids, window scrolling, sticky items, masonry, and several frameworks. Type signatures are visible beside behavior. The initial CSS contract is still easy to miss: a zero-height parent, absent relative spacer, or fixed height on a measured child can produce blank or jumping output without a direct diagnostic.
Maintenance5/5GitHub recorded a push on 2026-08-18, reports an unarchived repository, and counts 113 open issues and pull requests together. The same date brought virtual-core 3.17.8 fixes for stale scrolling state after cleanup and out-of-range measurement nodes. React, Vue, Solid, and Svelte adapters share that core, so measurement fixes do not have to be independently rediscovered in each framework package.
Ecosystem5/5npm counted 22,969,169 downloads for @tanstack/react-virtual from 2026-08-19 through 2026-08-25, and GitHub reported 7,088 stars. The peer range spans React and React DOM 16.8 through 19. Our measured package included types and loaded with both ESM import and require(), while TanStack Table examples give it a natural companion for large headless grids.

Use it if

  • A React 16.8 through 19 screen renders enough rows or cells that DOM and layout work visibly slows scrolling
  • Variable-height content can carry stable item keys, data-index attributes, and measurement refs
  • The design system must control every role, element, class, and interaction instead of adopting a finished list component
  • Window scrolling, horizontal layouts, grids, sticky ranges, or bottom anchoring should share 1 headless engine
Skip it if

Setup reality

We installed @tanstack/react-virtual 3.14.10 in 2.2 seconds on a clean Node 22 sandbox. The result was 5 packages and 9 MB on disk, with 0 known vulnerabilities from npm audit. The package declared 1 direct dependency plus 2 peers, React and React DOM, and its own unpacked files measured 96 KB. Bundled TypeScript declarations were present. ESM import and require() both loaded through its exports map under the How we test method.

We built the full package with esbuild at 36.4 KB minified and 11.5 KB gzipped. No credentials or config files are needed. The common blank-screen failure is CSS: the scroll element needs a bounded height and overflow, while the inner spacer needs position: relative and getTotalSize(). Each visible child then needs absolute positioning from its virtual start. Server rendering needs an initialRect or a planned short first render when layout shift is unacceptable.

For variable heights, place data-index and ref={virtualizer.measureElement} on the same node and do not force that node to estimateSize's height. Use a stable getItemKey when data can prepend, sort, or delete, or cached measurements follow the old array indexes. A low estimate makes the scrollbar resize as real rows arrive. Overscan reduces blank edges during fast movement at the cost of more mounted DOM.

Only rendered items participate in native search, printing, selection, and focus. Infinite loading needs a placeholder count and an in-flight guard so the last visible row does not trigger duplicate requests. Smooth scrolling and dynamic measurement interact badly because corrected sizes move the target during animation. React 19 users can disable the adapter's flushSync behavior when warnings appear, but that choice needs scrolling tests rather than a blanket flag change.

Patterns

Virtualize fixed-height rows render-fixed-rows

const parentRef = useRef<HTMLDivElement>(null)
const v = useVirtualizer({
  count: items.length,
  getScrollElement: () => parentRef.current,
  estimateSize: () => 35,
  overscan: 5,
})

return <div ref={parentRef} style={{height: 400, overflow: 'auto'}}>
  <div style={{height: v.getTotalSize(), position: 'relative'}}>
    {v.getVirtualItems().map(row => <div key={row.key} style={{position: 'absolute', width: '100%', height: row.size, transform: `translateY(${row.start}px)`}}>{items[row.index]}</div>)}
  </div>
</div>

All 3 layers matter: bounded scroll parent, relative total-size spacer, and absolutely positioned visible items.

Measure rows after layout measure-dynamic-rows

const v = useVirtualizer({
  count: messages.length,
  getScrollElement: () => parentRef.current,
  estimateSize: () => 60,
})

{v.getVirtualItems().map(item => <div
  key={item.key}
  data-index={item.index}
  ref={v.measureElement}
  style={{position: 'absolute', width: '100%', transform: `translateY(${item.start}px)`}}
>{messages[item.index].body}</div>)}

Put data-index and measureElement on the same node. Do not set its height to the 60 px estimate.

Tie virtualization to the document window use-window-scroll

const listRef = useRef<HTMLDivElement>(null)
const v = useWindowVirtualizer({
  count: posts.length,
  estimateSize: () => 240,
  overscan: 3,
  scrollMargin: listRef.current?.offsetTop ?? 0,
})

Subtract scrollMargin from each translated item start. It accounts for content above the virtual list.

Keep cached sizes attached to row IDs set-stable-keys

const getItemKey = useCallback((index: number) => rows[index].id, [rows])
const v = useVirtualizer({
  count: rows.length,
  getScrollElement: () => parentRef.current,
  estimateSize: () => 48,
  getItemKey,
})

Index keys assign old measurements to new content after a prepend or sort. Memoize a stable ID function.

Move to a virtual index scroll-to-row

v.scrollToIndex(500, {align: 'center'})
v.scrollToOffset(1200, {align: 'start'})
v.scrollToEnd({behavior: 'smooth'})

Long jumps use estimates until rows are measured. Smooth behavior is unreliable when dynamic sizes keep correcting the target.

Fetch when the final row becomes visible load-next-page

const visible = v.getVirtualItems()
useEffect(() => {
  const last = visible.at(-1)
  if (last && last.index >= rows.length - 1 && hasNextPage && !isFetchingNextPage) {
    fetchNextPage()
  }
}, [visible, rows.length, hasNextPage, isFetchingNextPage, fetchNextPage])

Guard both hasNextPage and the in-flight state. Add 1 placeholder to count when an unloaded row should occupy space.

Combine row and column virtualizers virtualize-two-axes

const rowsV = useVirtualizer({count: 10000, getScrollElement: () => parentRef.current, estimateSize: () => 32, overscan: 5})
const colsV = useVirtualizer({horizontal: true, count: 200, getScrollElement: () => parentRef.current, estimateSize: () => 100, overscan: 3})

Both instances can share 1 scroll element. Fixed widths and heights are easier than measuring both axes.

Force a section header into range keep-sticky-index

const rangeExtractor = useCallback((range: Range) => {
  const sticky = [...stickyIndexes].reverse().find(i => range.startIndex >= i) ?? 0
  return [...new Set([sticky, ...defaultRangeExtractor(range)])].sort((a, b) => a - b)
}, [])

const v = useVirtualizer({count: rows.length, getScrollElement: () => parentRef.current, estimateSize: () => 36, rangeExtractor})

rangeExtractor keeps the header mounted. Your CSS still owns sticky positioning and z-index.

Return to a measured scroll position restore-virtual-state

const saved = JSON.parse(sessionStorage.getItem('inbox') ?? 'null')
const v = useVirtualizer({
  count: rows.length,
  getScrollElement: () => parentRef.current,
  estimateSize: () => 48,
  initialMeasurementsCache: saved?.snapshot,
  initialOffset: saved?.offset,
})

Save takeSnapshot() and scrollOffset before leaving. Rows absent from the snapshot still use the 48 px estimate.

Alternatives

PackageRegistryPick it when
react-virtuosonpmUse it when React 19 screens need a component-level list with grouped and chat behavior
react-windownpmUse it for fixed-size lists and grids with a smaller surface
react-virtualizednpmUse it when an established application already relies on its broad component suite
virtuanpmUse it when automatic measurement is worth giving up some low-level layout control

More web frontend guides

postcss · react · react-dom · tailwindcss · htmlparser2 · tailwind-merge · the whole shelf →

How this guide is made: grounded in the library's documentation, release notes, changelog, and issue history, on a fixed rubric — not a hands-on install of every release. The 50 most-downloaded entries are additionally install-verified in clean containers. Corrections: contact the desk.