WritingVirtualizing Large Lists in React with react-window: An Advanced Guide — Clixo
6 min readreact, virtualization, react-window, performance, large-lists, advanced

Virtualizing Large Lists in React with react-window: An Advanced Guide

An advanced guide to virtualizing large lists in React using react-window — fixed and variable size lists, performance tuning, and integration with real data sources.

Rendering a list of a thousand rows in React mounts a thousand components into the DOM simultaneously. Even if each row is fast to render, the browser must layout, paint, and manage event listeners for all of them at once. Add memoization and you have reduced re-render cost, but the DOM is still bloated with nodes the user will never see. Memoization is the wrong tool here.

The correct tool is virtualization: only render the rows currently visible in the viewport. react-window is the most widely used library for this in React, and this guide covers how to use it correctly for both simple and complex scenarios.

How Virtualization Works

A virtualizing list measures the container's height and the scroll position, calculates which row indices are currently visible, and renders only those components. As the user scrolls, invisible rows are unmounted and new ones are mounted. The DOM stays small regardless of how many items are in the dataset.

This is not a rendering optimization in the traditional React sense — it is a structural change to how many components exist at any given time. A list of 10,000 rows might only have 30 mounted components at any moment.

Installing and Setting Up react-window

npm install react-window

react-window exports several components. The two most commonly used are:

  • FixedSizeList — all rows have the same height.
  • VariableSizeList — rows have different heights, provided by a sizing function.

FixedSizeList for Uniform Row Heights

FixedSizeList is the simpler and more performant option. Use it whenever all rows have the same height — chat messages of a fixed line height, table rows, card grids.

import { FixedSizeList } from "react-window";
 
function Row({ index, style }) {
  return (
    <div style={style}>
      Row {index}: {data[index].name}
    </div>
  );
}
 
function ItemList() {
  return (
    <FixedSizeList
      height={600}
      itemCount={data.length}
      itemSize={48}
      width="100%"
    >
      {Row}
    </FixedSizeList>
  );
}

The style prop injected into Row is critical — it contains the absolute positioning that places each row at the correct scroll offset. Never omit it from the row's root element.

height is the visible container height in pixels. itemSize is the height of each row in pixels. itemCount is the total number of items in the dataset.

VariableSizeList for Dynamic Row Heights

When rows have different heights — expandable list items, posts with variable text length, comment threads — use VariableSizeList. You provide a function that returns the height for a given index.

import { VariableSizeList } from "react-window";
 
const rowHeights = data.map(item => (item.expanded ? 120 : 48));
 
function getItemSize(index) {
  return rowHeights[index];
}
 
function VariableList() {
  const listRef = useRef();
 
  return (
    <VariableSizeList
      height={600}
      itemCount={data.length}
      itemSize={getItemSize}
      width="100%"
      ref={listRef}
    >
      {Row}
    </VariableSizeList>
  );
}

When row heights change at runtime — for example, when a user expands a row — call listRef.current.resetAfterIndex(changedIndex). Without this, VariableSizeList continues using stale height values and positions rows incorrectly.

Passing Data to Rows with itemData

Row components receive index and style as props. To pass additional data — the item array, click handlers, selected state — use the itemData prop on the list. The value is forwarded to each row component as a data prop.

const itemData = { items: data, onSelect: handleSelect };
 
function Row({ index, style, data }) {
  const item = data.items[index];
  return (
    <div style={style} onClick={() => data.onSelect(item.id)}>
      {item.name}
    </div>
  );
}
 
return (
  <FixedSizeList itemData={itemData} ...>
    {Row}
  </FixedSizeList>
);

This is preferable to capturing data in a closure, because itemData can be memoized with useMemo to keep the reference stable and prevent row re-renders.

Memoizing Row Components

Each row component should be wrapped in React.memo. Without it, every row re-renders when the parent list re-renders — even rows that are already mounted and whose data has not changed.

const Row = React.memo(function Row({ index, style, data }) {
  return (
    <div style={style}>
      {data.items[index].name}
    </div>
  );
});

Memoization at the row level is one of the few places where React.memo is almost always justified, because list items are exactly the pattern it was designed for: many similar components with stable, comparable props.

Handling Dynamic Lists and Scroll Restoration

For lists that load more data as the user scrolls — infinite scroll — react-window pairs with react-window-infinite-loader, which coordinates chunk loading with the virtualized scroll position.

For scroll restoration across navigation, save the scroll offset (exposed via onScroll) to a ref or state manager, and restore it using listRef.current.scrollTo(offset) when the component mounts.

When react-window Is Not the Right Tool

Virtualization adds complexity and has constraints: the list must be inside a fixed-height container, and absolute positioning for rows can conflict with some layout approaches. For lists under roughly 200 items, the performance gain from virtualization is often imperceptible, and the added complexity is not worth it.

If rows have unpredictable heights and you cannot measure them reliably before rendering, consider react-virtual (from TanStack), which supports dynamic measurement via intersection observers.

Combining Virtualization with Data Fetching

Virtualized lists work best with paginated or windowed data fetching: fetch the first N items, then fetch more as the user scrolls near the end. This keeps memory usage bounded on the client and avoids loading a dataset too large to manage efficiently.

Libraries like TanStack Query expose hasNextPage and fetchNextPage that integrate cleanly with infinite scroll patterns, complementing the rendering-layer virtualization that react-window provides.

Virtualization is one of the highest-leverage performance improvements available in React for data-heavy applications. Unlike memoization, which shaves milliseconds, removing thousands of DOM nodes from the tree has a structural impact on rendering, memory, and scroll performance.

If you are building a data-intensive React application and need engineering support on architecture and performance, Clixo builds production-grade software across the full stack.