# 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.

- **Published:** 2026-01-21
- **Author:** Clixo
- **Reading time:** 6 min read
- **Tags:** react, virtualization, react-window, performance, large-lists, advanced
- **Canonical URL:** https://clixo.sh/blog/react-virtualize-large-lists-react-window

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.

```mermaid
flowchart LR
  A["All Items (N rows)"] --> B[react-window]
  B --> C[Measure container height and scroll offset]
  C --> D[Compute visible index range]
  D --> E["Mount only visible rows (20-40 nodes)"]
  E --> F[User scrolls]
  F --> G[Unmount out-of-view rows]
  G --> H[Mount newly visible rows]
  H --> F
```

## Installing and Setting Up react-window

```bash
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.

```js
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.

```js
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.

```js
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.

```js
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](https://clixo.sh/#contact) across the full stack.

---

Clixo · 1141 W Bryn Mawr Ave, Itasca, IL 60143, US · [hello@clixo.sh](mailto:hello@clixo.sh)
[Start a build](https://clixo.sh/#contact) · [All services](https://clixo.sh/services) · [Agent guide (llms.txt)](https://clixo.sh/llms.txt)
