Skip to content
Codeloom
React

Virtualizing Large Lists in React with react-window

Learn how to render thousands of items efficiently using react-window for list and grid virtualization in React applications.

·5 min read · By Codeloom
Intermediate 10 min read

What you'll learn

  • Why virtualization matters for large lists
  • Setting up react-window with FixedSizeList
  • Variable-height rows with VariableSizeList
  • Grid virtualization for two-dimensional data
  • Integrating with infinite scroll and search

Prerequisites

  • Basic React knowledge
  • Familiarity with component props and rendering

Rendering a list of 10,000 items means creating 10,000 DOM nodes. The browser slows down, scrolling stutters, and memory usage spikes. Virtualization solves this by only rendering the items currently visible in the viewport, plus a small buffer above and below. The react-window library makes this straightforward.

Why Not Just Render Everything

Every DOM node costs memory and layout computation. A list of 1,000 simple <div> elements might be fine, but add images, event listeners, or nested components and performance degrades quickly. Virtualization keeps the DOM node count constant regardless of list size. Scroll through 100 items or 100,000, and the browser always manages the same small set of nodes.

Installing react-window

npm install react-window

The library is small (about 6 KB gzipped) and has no dependencies.

FixedSizeList: Uniform Row Heights

When every row has the same height, use FixedSizeList. It is the simplest and most performant option.

import { FixedSizeList } from 'react-window';

const users = Array.from({ length: 10000 }, (_, i) => ({
  id: i,
  name: `User ${i + 1}`,
  email: `user${i + 1}@example.com`,
}));

function Row({ index, style }) {
  const user = users[index];
  return (
    <div style={style} className="row">
      <span className="name">{user.name}</span>
      <span className="email">{user.email}</span>
    </div>
  );
}

function UserList() {
  return (
    <FixedSizeList
      height={600}
      width="100%"
      itemCount={users.length}
      itemSize={50}
    >
      {Row}
    </FixedSizeList>
  );
}

The style prop is critical. It positions each row absolutely within the scrollable container. Never override the top or height properties in the style, or the layout will break.

The height prop sets the viewport height. The itemSize prop is the fixed height of each row in pixels. react-window calculates the total scrollable height and only renders the rows currently visible.

Passing Data Through itemData

Avoid closing over external arrays in the Row component. Instead, use the itemData prop to pass data explicitly. This enables memoization.

import { FixedSizeList } from 'react-window';
import { memo } from 'react';

const Row = memo(function Row({ index, style, data }) {
  const user = data[index];
  return (
    <div style={style} className="row">
      <strong>{user.name}</strong>
      <span>{user.email}</span>
    </div>
  );
});

function UserList({ users }) {
  return (
    <FixedSizeList
      height={600}
      width="100%"
      itemCount={users.length}
      itemSize={50}
      itemData={users}
    >
      {Row}
    </FixedSizeList>
  );
}

Wrapping Row in memo prevents re-renders when the parent updates but the row data has not changed.

VariableSizeList: Dynamic Row Heights

When rows have different heights, use VariableSizeList. You provide a function that returns the height for each index.

import { VariableSizeList } from 'react-window';

const messages = [
  { id: 1, text: 'Hello' },
  { id: 2, text: 'This is a longer message that wraps to multiple lines and takes more vertical space in the UI.' },
  { id: 3, text: 'Short' },
  // ... thousands more
];

function getItemSize(index) {
  // Estimate height based on text length
  const text = messages[index].text;
  const lineCount = Math.ceil(text.length / 60);
  return 24 + lineCount * 20; // base padding + lines
}

function MessageRow({ index, style }) {
  return (
    <div style={style} className="message">
      {messages[index].text}
    </div>
  );
}

function MessageList() {
  return (
    <VariableSizeList
      height={500}
      width="100%"
      itemCount={messages.length}
      itemSize={getItemSize}
    >
      {MessageRow}
    </VariableSizeList>
  );
}

If the height depends on rendered content (which you cannot know ahead of time), you may need to measure rows dynamically. The react-virtualized-auto-sizer and react-window communities offer solutions for this, though it adds complexity.

FixedSizeGrid: Two-Dimensional Virtualization

For grids, FixedSizeGrid virtualizes both rows and columns.

import { FixedSizeGrid } from 'react-window';

function Cell({ columnIndex, rowIndex, style }) {
  return (
    <div style={style} className="cell">
      Row {rowIndex}, Col {columnIndex}
    </div>
  );
}

function DataGrid() {
  return (
    <FixedSizeGrid
      columnCount={50}
      columnWidth={150}
      height={600}
      rowCount={1000}
      rowHeight={40}
      width={800}
    >
      {Cell}
    </FixedSizeGrid>
  );
}

This renders a spreadsheet-like view where only the visible cells exist in the DOM. Scrolling horizontally and vertically is smooth even with 50,000 cells.

Auto-Sizing the Container

Hard-coding the list height is fragile. Use react-virtualized-auto-sizer to make the list fill its parent container.

npm install react-virtualized-auto-sizer
import { FixedSizeList } from 'react-window';
import AutoSizer from 'react-virtualized-auto-sizer';

function UserList({ users }) {
  return (
    <div style={{ height: '100%', width: '100%' }}>
      <AutoSizer>
        {({ height, width }) => (
          <FixedSizeList
            height={height}
            width={width}
            itemCount={users.length}
            itemSize={50}
            itemData={users}
          >
            {Row}
          </FixedSizeList>
        )}
      </AutoSizer>
    </div>
  );
}

The parent element must have a defined height (via CSS or flex layout) for AutoSizer to work.

Infinite Scroll with react-window-infinite-loader

To load more data as the user scrolls, combine react-window with react-window-infinite-loader.

npm install react-window-infinite-loader
import { FixedSizeList } from 'react-window';
import InfiniteLoader from 'react-window-infinite-loader';

function InfiniteList({ items, hasMore, loadMore }) {
  const itemCount = hasMore ? items.length + 1 : items.length;
  const isItemLoaded = (index) => !hasMore || index < items.length;

  return (
    <InfiniteLoader
      isItemLoaded={isItemLoaded}
      itemCount={itemCount}
      loadMoreItems={loadMore}
    >
      {({ onItemsRendered, ref }) => (
        <FixedSizeList
          ref={ref}
          height={600}
          width="100%"
          itemCount={itemCount}
          itemSize={50}
          onItemsRendered={onItemsRendered}
        >
          {({ index, style }) => (
            <div style={style}>
              {isItemLoaded(index) ? items[index].name : 'Loading...'}
            </div>
          )}
        </FixedSizeList>
      )}
    </InfiniteLoader>
  );
}

The loader calls loadMore when the user scrolls near unloaded items.

When to Virtualize

Virtualize when you have more than a few hundred items with complex row components, or more than a few thousand items with simple rows. For shorter lists, the overhead of virtualization (absolute positioning, scroll handling) is not worth it. Measure first: if the list scrolls smoothly without virtualization, keep it simple.