mrkeyoor.com_
Wed 23 Sept 00:34 UTC
npmWeb Frontendupdated 22 Sept 2026

react-virtuoso review

react-virtuoso 4.18.12 renders the visible window of a large React collection and measures row sizes from the DOM. `Virtuoso` handles variable-height lists, `GroupedVirtuoso` adds sticky groups, `VirtuosoGrid` lays out equal-size cards, and `TableVirtuoso` keeps table semantics. It supports loading at either edge, bottom-following feeds, scroll restoration, window scrolling, SSR seeds, and imperative navigation. Version 4.18.12 measures window-scrolling lists before they enter the viewport so their estimated height already participates in document layout. Our full-import browser build measured 72.6 KB minified and 23.9 KB gzipped, so use it when avoided row rendering repays that cost.

Verdict

react-virtuoso 4.18.12 installed in 1.4 seconds with 0 direct dependencies and no audit findings, but our full browser import was 23.9 KB gzipped. That trade fits large variable-height lists, tables, and reverse-loading feeds; render small lists normally and choose a headless virtualizer when owning layout is the point.

We installed it

Lab card: what happened when we installed react-virtuosoScreenshot of react-virtuoso documentation
Install✓ · 1.4s4 packages on disk · 8 MB
ImportESM import works · require() works · CommonJS package with exports map
Browser23.9 KBgzipped (72.6 KB minified), bundled with esbuild
TypesTypeScript types bundled
Known vulns00 critical · 0 high · 0 moderate · 0 low (npm audit)

Answers from our run

Does react-virtuoso install cleanly?

Yes. In a fresh container with an empty cache, npm install react-virtuoso finished in 1 seconds, leaving 4 packages and 8 MB on disk. npm audit reported no known vulnerabilities.

How much does react-virtuoso add to a browser bundle?

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

Does react-virtuoso work with both ESM and CommonJS?

Yes. Both import 'react-virtuoso' and require('react-virtuoso') worked in Node 22 in our run. The package is published as CommonJS with an exports map.

Does react-virtuoso include TypeScript types?

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

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

@tanstack/react-virtual: Choose it for a headless virtualizer when your code should own markup, scroll containers, and layout behavior. react-virtuoso 4.18.12 installed in 1.4 seconds with 0 direct dependencies and no audit findings, but our full browser import was 23.9 KB gzipped.

When should you not use react-virtuoso?

The collection is a few dozen cheap rows. Normal rendering is easier to test and search, while our full import added 23.9 KB gzipped.

API stability4/5The 4.x line keeps `data`, `totalCount`, `itemContent`, component overrides, and imperative handles at the center while adding focused props. The package exposes one typed root with explicit ESM and CommonJS targets. Patch releases still alter observable scrolling and measurement behavior: recent work touched window-list layout, React 19 subscriptions, RTL positioning, bottom detection, and index clamping. Visual and interaction regression tests are warranted even without a signature change.
Docs5/5virtuoso.dev has live typed examples for flat, grouped, table, grid, endless, reverse, window, SSR, custom component, and test cases, followed by generated prop references. The troubleshooting material names concrete failures such as margins, zero-size elements, ref mistakes, ResizeObserver noise, and dynamic media. Product pages also separate the MIT list core from the commercial Message List, which prevents a reader from assuming the chat API ships under the same terms.
Maintenance5/5Version 4.18.12 and the latest GitHub push both landed on August 17, 2026. GitHub reports 6,451 stars, 56 open issues and pull requests, and an unarchived repository. The current patch fixes document layout for offscreen window-scrolling lists, while earlier 4.18 patches addressed React 19, server layout, RTL lists, bottom state, scrolling bounds, and table types. The fixes track current browser and React behavior rather than only dependency churn.
Ecosystem5/5npm counted 3,402,412 downloads in the week ending August 24, 2026. The package peers across React 16, 17, 18, and 19, publishes both module formats and types, and has official examples for Material UI and TanStack Table. Standard DOM output works with ordinary CSS systems. No direct runtime dependencies keeps package coupling low, though chat and masonry capabilities increasingly sit in separate Virtuoso products.

Use it if

  • Hundreds or thousands of React rows have changing heights and maintaining a measurement cache yourself is unwanted work.
  • One package should cover virtual flat lists, sticky grouped lists, equal-size grids, and semantic tables.
  • A feed or log viewer must load at both ends, preserve position on prepend, or follow output only while the reader remains at the bottom.
  • React 16 through 19 support, bundled TypeScript declarations, and both module formats are useful across a mixed frontend estate.
Skip it if

Setup reality

We installed react-virtuoso 4.18.12 in 1.4 seconds in a fresh Node 22 Bookworm sandbox. It left 4 packages and 8 MB on disk. The package itself was 256 KB unpacked with no direct dependencies and 2 peer dependencies, React and React DOM. npm audit found 0 known vulnerabilities. The CommonJS package has an exports map, includes TypeScript declarations, and worked through both require() and ESM import. Our full namespace browser bundle measured 72.6 KB minified and 23.9 KB gzipped.

Setup needs no stylesheet or provider, and it has no native compilation, secret, or config file. A list does need a real viewport height from inline style, CSS, a flex layout, window scrolling, or a custom scroll parent. Percentage heights only work when ancestors define their own height. The first rendered row seeds the size estimate. Set defaultItemHeight when that row is an outlier; use fixedItemHeight only when every row truly has one fixed height.

DOM geometry is the recurring trap. ResizeObserver catches later changes, but measured rectangles exclude vertical margins, so use padding and reset margins on paragraphs, headings, lists, blockquotes, and code blocks inside rows. Filter zero-height items instead of hiding them in the virtual data. Give data records stable keys through computeItemKey. Version 4.18.12 improves offscreen window-list document sizing, but images and iframes that load late can still disturb reverse-scroll position.

endReached and startReached do not fetch, cancel, deduplicate, or serialize requests; guard them in application state. Prepending requires decreasing a positive firstItemIndex by exactly the inserted data count. SSR's initialItemCount only emits an initial unmeasured slice. JSDOM supplies no useful layout, so tests need VirtuosoMockContext or the grid equivalent. Define custom component types outside render and forward refs. The MIT core has feed primitives, while the packaged Message List and masonry products have separate install and license decisions.

Patterns

Render typed rows with stable identity render-variable-list

import {Virtuoso} from 'react-virtuoso';

type User = {id: string; name: string; bio: string};

export function UserList({users}: {users: User[]}) {
  return (
    <Virtuoso
      style={{height: 480}}
      data={users}
      computeItemKey={(_index, user) => user.id}
      itemContent={(_index, user) => (
        <article style={{padding: 12}}>
          <strong>{user.name}</strong>
          <p style={{margin: 0}}>{user.bio}</p>
        </article>
      )}
    />
  );
}

`data` supplies the count and item type. Stable domain keys preserve row identity, while padding avoids the vertical-margin measurement problem.

Virtualize a large index-only collection render-index-range

<Virtuoso
  style={{height: 400}}
  totalCount={100_000}
  defaultItemHeight={36}
  itemContent={(index) => (
    <div style={{padding: 8}}>Row {index}</div>
  )}
/>

`defaultItemHeight` replaces the first row as the initial estimate. `fixedItemHeight` should be used only when every rendered row is exactly that height.

Guard an end-reached fetch against overlap load-next-page

const [loading, setLoading] = useState(false);

const loadMore = useCallback(async () => {
  if (loading || !hasNextPage) return;
  setLoading(true);
  try {
    const next = await fetchNextPage();
    setItems((current) => [...current, ...next]);
  } finally {
    setLoading(false);
  }
}, [loading, hasNextPage]);

return <Virtuoso style={{height: 500}} data={items} endReached={loadMore} itemContent={renderItem} />;

`endReached` may fire again after data or viewport changes. The application must serialize requests and deduplicate returned records.

Navigate through the imperative list handle scroll-to-index

import {useRef} from 'react';
import {Virtuoso, type VirtuosoHandle} from 'react-virtuoso';

const listRef = useRef<VirtuosoHandle>(null);

<button onClick={() => listRef.current?.scrollToIndex({
  index: 500,
  align: 'center',
  behavior: 'smooth',
})}>Go to row 500</button>

<Virtuoso ref={listRef} style={{height: 500}} totalCount={1000} itemContent={(index) => <div>{index}</div>} />

Out-of-range indexes are clamped in current releases. Smooth motion across many unmeasured variable rows can still look expensive.

Mount with the final message in view start-at-item

<Virtuoso
  style={{height: 500}}
  data={messages}
  initialTopMostItemIndex={{
    index: messages.length - 1,
    align: 'end',
  }}
  itemContent={renderMessage}
/>

`initialTopMostItemIndex` controls mount placement. Use the handle's `scrollToIndex` for later navigation instead of changing this initial prop.

Follow appended rows only at the bottom follow-live-output

<Virtuoso
  style={{height: 500}}
  data={messages}
  alignToBottom
  followOutput={(isAtBottom) =>
    isAtBottom ? 'smooth' : false
  }
  itemContent={(_index, message) => (
    <MessageRow message={message} />
  )}
/>

This bottom-follow primitive is part of the MIT package. The higher-level Message List data API is a separately licensed product.

Insert older records without moving the viewport prepend-history

const INITIAL_INDEX = 100_000;
const [firstItemIndex, setFirstItemIndex] = useState(INITIAL_INDEX);

async function loadOlder() {
  const older = await fetchOlder();
  setFirstItemIndex((value) => value - older.length);
  setMessages((current) => [...older, ...current]);
}

return <Virtuoso
  style={{height: 500}}
  firstItemIndex={firstItemIndex}
  data={messages}
  startReached={loadOlder}
  itemContent={renderMessage}
/>;

Keep `firstItemIndex` positive and subtract exactly the inserted data count. Guard `startReached` so two history requests cannot overlap.

Add sticky headers to flattened groups render-groups

import {GroupedVirtuoso} from 'react-virtuoso';

<GroupedVirtuoso
  style={{height: 500}}
  groupCounts={[3, 2, 4]}
  groupContent={(groupIndex) => (
    <div style={{background: 'white', padding: 8}}>
      {groupNames[groupIndex]}
    </div>
  )}
  itemContent={(itemIndex) => (
    <ContactRow contact={contacts[itemIndex]} />
  )}
/>

`groupCounts` stores the number of data rows per group. `itemContent` receives the flattened item index, not an index local to its group.

Virtualize equal-size cards with CSS columns render-equal-grid

import {VirtuosoGrid} from 'react-virtuoso';

<VirtuosoGrid
  style={{height: 600}}
  data={products}
  computeItemKey={(_index, product) => product.id}
  listClassName="product-grid"
  itemClassName="product-grid-item"
  itemContent={(_index, product) => (
    <ProductCard product={product} />
  )}
/>

CSS owns columns and item dimensions. `VirtuosoGrid` assumes equal-size items, so uneven cards need the separate masonry package.

Keep table markup with a sticky heading row render-table

import {TableVirtuoso} from 'react-virtuoso';

<TableVirtuoso
  style={{height: 520}}
  data={users}
  fixedHeaderContent={() => (
    <tr><th>Name</th><th>Email</th></tr>
  )}
  itemContent={(_index, user) => (
    <>
      <td>{user.name}</td>
      <td>{user.email}</td>
    </>
  )}
/>

Retain normal table display rules. `border-collapse: collapse` can make sticky borders scroll away; the docs recommend separate borders.

Emit twelve rows before browser measurement seed-server-render

<Virtuoso
  style={{height: 500}}
  data={articles}
  initialItemCount={12}
  itemContent={(_index, article) => (
    <ArticleRow article={article} />
  )}
/>

`initialItemCount` creates an unmeasured server slice, not a permanent page size. Reserve stable row space to limit layout shift after hydration.

Give JSDOM deterministic list measurements mock-dom-sizes

import {render} from '@testing-library/react';
import {Virtuoso, VirtuosoMockContext} from 'react-virtuoso';

const result = render(
  <Virtuoso data={users} itemContent={renderUser} />,
  {
    wrapper: ({children}) => (
      <VirtuosoMockContext.Provider
        value={{viewportHeight: 300, itemHeight: 50}}
      >
        {children}
      </VirtuosoMockContext.Provider>
    ),
  }
);

expect(result.getByText(users[0].name)).toBeVisible();

JSDOM does not calculate layout, so an unmocked virtual list can render no rows. Grid tests use `VirtuosoGridMockContext` with width measurements.

Alternatives

PackageRegistryPick it when
@tanstack/react-virtualnpmChoose it for a headless virtualizer when your code should own markup, scroll containers, and layout behavior.
react-windownpmChoose it for a smaller fixed-size list or grid whose item dimensions are already known.
react-virtualizednpmChoose it when maintaining a legacy app already built around its broad list, grid, table, and measurement APIs.
@virtuoso.dev/masonrynpmChoose it when variable-height cards must pack into masonry columns rather than an equal-size grid.

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.