react-list vs react-infinite vs react-virtual vs react-virtualized vs react-window vs react-window-infinite-loader
Virtualizing Long Lists in React: Architecture and Performance
react-listreact-infinitereact-virtualreact-virtualizedreact-windowreact-window-infinite-loaderSimilar Packages:

Virtualizing Long Lists in React: Architecture and Performance

These libraries solve the performance bottleneck of rendering large datasets in React by using a technique called windowing or virtualization. Instead of rendering every item in a list, they only render the items currently visible in the viewport. This keeps the DOM light and ensures smooth scrolling even with thousands of records. While they share the same goal, they differ significantly in API design, bundle size, maintenance status, and flexibility.

Npm Package Weekly Downloads Trend

3 Years

Github Stars Ranking

Stat Detail

Package
Downloads
Stars
Size
Issues
Publish
License
react-list500,7561,97434.9 kB71a year agoMIT
react-infinite02,686243 kB102-BSD-3-Clause
react-virtual06,853158 kB102-MIT
react-virtualized027,0692.24 MB0a year agoMIT
react-window017,160209 kB22 months agoMIT
react-window-infinite-loader095123 kB03 months agoMIT

Virtualizing Long Lists in React: Architecture and Performance

When your React app needs to display thousands of rows — like a data grid, a chat log, or a feed — rendering every single DOM node will crash the browser. These six packages solve that problem by only rendering what the user can see. However, they approach the problem differently. Some use components, some use hooks, and some are no longer maintained. Let's break down the technical differences.

đŸ—ī¸ Architecture: Components vs Hooks

The biggest split in this group is how you integrate them into your app. react-window, react-virtualized, react-list, and react-infinite use components. You wrap your content in their provided components, and they handle the math.

react-virtual uses a hook. It gives you the measurements and you render the elements yourself. This offers more control but requires more setup.

Component-Based API

react-window uses a clean, function-component approach. You pass the item size and count, and it renders a window of items.

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

const Row = ({ index, style }) => (
  <div style={style}>Row {index}</div>
);

const List = () => (
  <FixedSizeList
    height={500}
    itemCount={1000}
    itemSize={35}
    width={300}
  >
    {Row}
  </FixedSizeList>
);

react-virtualized uses a similar component pattern but with more verbose prop names. It was the industry standard for years.

// react-virtualized
import { List } from 'react-virtualized';

const Row = ({ index, key, style }) => (
  <div key={key} style={style}>Row {index}</div>
);

const List = () => (
  <List
    height={500}
    rowCount={1000}
    rowHeight={35}
    rowRenderer={Row}
    width={300}
  />
);

react-list simplifies the API further, focusing on ease of use for standard lists.

// react-list
import List from 'react-list';

const Item = ({ key, index }) => (
  <div key={key}>Item {index}</div>
);

const MyList = ({ items }) => (
  <List
    itemRenderer={Item}
    length={items.length}
  />
);

react-infinite uses a container approach where you define the element height explicitly.

// react-infinite
import Infinite from 'react-infinite';

const MyInfinite = () => (
  <Infinite
    elementHeight={50}
    containerHeight={500}
  >
    <div>Item 1</div>
    <div>Item 2</div>
  </Infinite>
);

Hook-Based API

react-virtual (TanStack) provides a hook that returns virtualizer state. You map over the virtual items and apply styles manually.

// react-virtual (@tanstack/react-virtual)
import { useVirtual } from '@tanstack/react-virtual';
import { useRef } from 'react';

const VirtualList = ({ items }) => {
  const parentRef = useRef();

  const virtualizer = useVirtual({
    count: items.length,
    getScrollElement: () => parentRef.current,
    estimateSize: () => 35,
  });

  return (
    <div ref={parentRef} style={{ height: 500, overflow: 'auto' }}>
      <div
        style={{
          height: `${virtualizer.getTotalSize()}px`,
          width: '100%',
          position: 'relative',
        }}
      >
        {virtualizer.getVirtualItems().map((virtualItem) => (
          <div
            key={virtualItem.key}
            style={{
              position: 'absolute',
              top: 0,
              left: 0,
              width: '100%',
              height: `${virtualItem.size}px`,
              transform: `translateY(${virtualItem.start}px)`,
            }}
          >
            Item {virtualItem.index}
          </div>
        ))}
      </div>
    </div>
  );
};

đŸ“Ļ Infinite Loading Patterns

Loading data as the user scrolls requires tracking the scroll position and triggering fetches. react-window-infinite-loader is built specifically for this, while others require manual implementation.

react-window-infinite-loader wraps a react-window component. It tells you which items need loading.

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

const LoadingList = ({ items, loadMoreItems }) => (
  <InfiniteLoader
    isItemLoaded={(index) => !!items[index]}
    itemCount={items.length}
    loadMoreItems={loadMoreItems}
  >
    {({ onItemsRendered, ref }) => (
      <FixedSizeList
        height={500}
        itemCount={items.length}
        itemSize={35}
        onItemsRendered={onItemsRendered}
        ref={ref}
        width={300}
      >
        {Row}
      </FixedSizeList>
    )}
  </InfiniteLoader>
);

react-infinite has infinite loading built into its core component via props.

// react-infinite
<Infinite
  elementHeight={50}
  containerHeight={500}
  onInfiniteLoad={() => loadMoreData()}
>
  {items.map(item => <div key={item.id}>{item.name}</div>)}
</Infinite>

react-virtualized requires you to use the InfiniteLoader component included in the library (similar concept to react-window's loader).

// react-virtualized
import { InfiniteLoader, List } from 'react-virtualized';

<InfiniteLoader
  rowCount={items.length}
  rowHeight={35}
  isRowLoaded={({ index }) => !!items[index]}
  loadMoreRows={loadMoreData}
>
  {({ onRowsRendered, registerChild }) => (
    <List
      height={500}
      onRowsRendered={onRowsRendered}
      ref={registerChild}
      rowCount={items.length}
      rowHeight={35}
      rowRenderer={Row}
      width={300}
    />
  )}
</InfiniteLoader>

react-virtual requires manual scroll event handling to trigger loads.

// react-virtual
const VirtualList = ({ items, loadMore }) => {
  const parentRef = useRef();
  const virtualizer = useVirtual({ count: items.length, getScrollElement: () => parentRef.current });

  const handleScroll = () => {
    const { scrollHeight, scrollTop, clientHeight } = parentRef.current;
    if (scrollHeight - scrollTop - clientHeight < 100) {
      loadMore();
    }
  };

  return (
    <div ref={parentRef} onScroll={handleScroll} style={{ height: 500, overflow: 'auto' }}>
      {/* render items */}
    </div>
  );
};

react-list does not have built-in infinite loading helpers; you must implement scroll listeners manually similar to react-virtual.

// react-list
// Manual implementation required for infinite loading
const MyList = ({ items, loadMore }) => {
  const listRef = useRef();
  
  const handleScroll = () => {
     // Check scroll position and call loadMore()
  };

  return (
    <div onScroll={handleScroll}>
      <List itemRenderer={Item} length={items.length} ref={listRef} />
    </div>
  );
};

đŸ› ī¸ Variable Row Sizes

Handling items with different heights is one of the hardest problems in virtualization. You need to measure items to know where to position them.

react-window uses VariableSizeList. You must tell it when sizes change.

// react-window
import { VariableSizeList } from 'react-window';

const listRef = useRef();

<VariableSizeList
  height={500}
  itemCount={items.length}
  itemSize={(index) => getHeightForItem(items[index])}
  ref={listRef}
  width={300}
>
  {Row}
</VariableSizeList>

// Notify library when size changes
listRef.current.resetAfterIndex(index);

react-virtualized uses CellMeasurer to calculate sizes dynamically.

// react-virtualized
import { CellMeasurer, CellMeasurerCache, List } from 'react-virtualized';

const cache = new CellMeasurerCache({ defaultHeight: 50, fixedWidth: true });

<List
  height={500}
  rowCount={1000}
  rowHeight={cache.rowHeight}
  rowRenderer={({ index, key, parent, style }) => (
    <CellMeasurer cache={cache} index={index} key={key} parent={parent}>
      {({ measure, registerChild }) => (
        <div ref={registerChild} onLoad={measure} style={style}>
          Content
        </div>
      )}
    </CellMeasurer>
  )}
  width={300}
/>

react-virtual handles dynamic sizes via the estimateSize function and measures elements automatically if configured.

// react-virtual
const virtualizer = useVirtual({
  count: items.length,
  getScrollElement: () => parentRef.current,
  estimateSize: (index) => 50, // Initial estimate
  measureElement: (element) => element.offsetHeight,
});

react-list supports variable items but requires the useStaticItemHeights prop to be false, and it may be less performant on very large datasets.

// react-list
<List
  itemRenderer={Item}
  length={items.length}
  useStaticItemHeights={false}
/>

react-infinite requires a fixed elementHeight prop. It does not support variable heights natively without complex workarounds.

// react-infinite
// No direct support for variable heights in standard API
<Infinite elementHeight={50} containerHeight={500}>
  {/* All children assumed to be 50px */}
</Infinite>

âš ī¸ Maintenance and Status

This is the most critical factor for architectural decisions.

  • react-virtualized: Maintenance Mode. The creator, Brian Vaughn, has moved focus to react-window. It is stable but will not get new features. The bundle size is significantly larger (~90KB) compared to react-window (~13KB).
  • react-window: Active. This is the recommended standard for component-based virtualization. It is lightweight and actively maintained.
  • react-virtual: Active. Now part of TanStack. It is the best choice for hook-based needs and supports multiple frameworks.
  • react-infinite: Legacy. Last major updates were years ago. It relies on older React patterns (createReactClass) and is not suitable for modern React (Hooks/Concurrent Mode).
  • react-list: Low Activity. It works, but development has slowed. It is fine for simple internal tools but risky for complex consumer-facing apps.
  • react-window-infinite-loader: Active. Maintained alongside react-window.

📊 Summary Comparison

Featurereact-windowreact-virtualizedreact-virtualreact-infinite
API StyleComponentComponentHookComponent
Bundle SizeSmall (~13KB)Large (~90KB)SmallMedium
Variable SizeYes (Manual notify)Yes (CellMeasurer)Yes (Auto measure)No
Infinite LoadVia Loader PackageBuilt-in ComponentManualBuilt-in Prop
Status✅ Activeâš ī¸ Maintenance✅ Active❌ Legacy

💡 Final Recommendation

For most applications today, react-window is the safest bet. It balances ease of use with performance and has a clear path for infinite loading via its companion package. If you need to customize the DOM structure heavily or share logic across frameworks, react-virtual is the superior modern choice.

Avoid react-virtualized unless you are maintaining an existing codebase that already uses it. Avoid react-infinite entirely for new work. The ecosystem has moved toward smaller, more focused tools that play nicely with React's concurrent features.

How to Choose: react-list vs react-infinite vs react-virtual vs react-virtualized vs react-window vs react-window-infinite-loader

  • react-list:

    Choose react-list if you need a lightweight, simple solution for basic lists without complex windowing logic. It is suitable for small to medium-sized lists where full virtualization might be overkill, but be aware it has fewer features for dynamic sizing compared to react-window.

  • react-infinite:

    Avoid using react-infinite for new projects. It is a legacy library that lacks modern React support and performance optimizations found in newer alternatives. It may still work for very simple, static infinite scrolls, but you will likely encounter issues with dynamic content or complex layouts.

  • react-virtual:

    Choose react-virtual (now @tanstack/react-virtual) if you prefer a hook-based API that gives you full control over the DOM elements. It is ideal for custom layouts, non-standard scrolling containers, or when you need framework-agnostic logic that works with Vue or Svelte as well.

  • react-virtualized:

    Do not choose react-virtualized for new projects. It is officially in maintenance mode and has been superseded by react-window. While it is stable and feature-rich, its bundle size is larger, and it will not receive new features or significant updates.

  • react-window:

    Choose react-window for most standard virtualization needs. It is the modern successor to react-virtualized, offering a smaller bundle size and a cleaner API. It is the best choice for fixed-size or estimated variable-size lists in standard scrollable containers.

  • react-window-infinite-loader:

    Choose react-window-infinite-loader specifically when using react-window and you need to fetch data as the user scrolls. It wraps react-window components to handle loading states and data fetching automatically, making infinite scrolling implementation much simpler.

README for react-list

ReactList

A versatile infinite scroll React component.

Install

bower install react-list

# or

npm install react-list

ReactList depends on React.

Examples

Check out the example page and the the example page source for examples of different configurations.

Here's another simple example to get you started.

import loadAccount from 'my-account-loader';
import React from 'react';
import ReactList from 'react-list';

class MyComponent extends React.Component {
  state = {
    accounts: []
  };

  componentWillMount() {
    loadAccounts(::this.handleAccounts);
  }

  handleAccounts(accounts) {
    this.setState({accounts});
  }

  renderItem(index, key) {
    return <div key={key}>{this.state.accounts[index].name}</div>;
  }

  render() {
    return (
      <div>
        <h1>Accounts</h1>
        <div style={{overflow: 'auto', maxHeight: 400}}>
          <ReactList
            itemRenderer={::this.renderItem}
            length={this.state.accounts.length}
            type='uniform'
          />
        </div>
      </div>
    );
  }
}

Props

axis (defaults to y)

The axis that this list scrolls on.

initialIndex

An index to scroll to after mounting.

itemRenderer(index, key)

A function that receives an index and a key and returns the content to be rendered for the item at that index.

itemsRenderer(items, ref)

A function that receives the rendered items and a ref. By default this element is just a <div>. Generally it only needs to be overridden for use in a <table> or other special case. NOTE: You must set ref={ref} on the component that contains the items so the correct item sizing calculations can be made.

itemSizeEstimator(index, cache)

A function that receives an item index and the cached known item sizes and returns an estimated size (height for y-axis lists and width for x-axis lists) of that item at that index. This prop is only used when the prop type is set to variable and itemSizeGetter is not defined. Use this property when you can't know the exact size of an item before rendering it, but want it to take up space in the list regardless.

itemSizeGetter(index)

A function that receives an item index and returns the size (height for y-axis lists and width for x-axis lists) of that item at that index. This prop is only used when the prop type is set to variable.

length (defaults to 0)

The number of items in the list.

minSize (defaults to 1)

The minimum number of items to render at any given time. This can be used to render some amount of items initially when rendering HTML on the server.

pageSize (defaults to 10)

The number of items to batch up for new renders. Does not apply to 'uniform' lists as the optimal number of items is calculated automatically.

scrollParentGetter (defaults to finding the nearest scrollable parent)

A function that returns a DOM Element or Window that will be treated as the scrolling container for the list. In most cases this does not need to be set for the list to work as intended. It is exposed as a prop for more complicated uses where the scrolling container may not initially have an overflow property that enables scrolling.

scrollParentViewportSizeGetter (defaults to scrollParent's viewport size)

A function that returns the size of the scrollParent's viewport. Provide this prop if you can efficiently determine your scrollParent's viewport size as it can improve performance.

threshold (defaults to 100)

The number of pixels to buffer at the beginning and end of the rendered list items.

type (one of simple, variable, or uniform, defaults to simple)
  • simple This type is...simple. It will not cache item sizes or remove items that are above the viewport. This type is sufficient for many cases when the only requirement is incremental rendering when scrolling.

  • variable This type is preferred when the sizes of the items in the list vary. Supply the itemSizeGetter when possible so the entire length of the list can be established beforehand. Otherwise, the item sizes will be cached as they are rendered so that items that are above the viewport can be removed as the list is scrolled.

  • uniform This type is preferred when you can guarantee all of your elements will be the same size. The advantage here is that the size of the entire list can be calculated ahead of time and only enough items to fill the viewport ever need to be drawn. The size of the first item will be used to infer the size of every other item. Multiple items per row are also supported with this type.

useStaticSize (defaults to false)

Set to true if the item size will never change (as a result of responsive layout changing or otherwise). This prop is only used when the prop type is set to uniform. This is an opt-in optimization that will cause the very first element's size to be used for all elements for the duration of the component's life.

useTranslate3d (defaults to false)

A boolean to determine whether the translate3d CSS property should be used for positioning instead of the default translate. This can help performance on mobile devices, but is supported by fewer browsers.

Methods

scrollTo(index)

Put the element at index at the top of the viewport. Note that if you aren't using type='uniform' or an itemSizeGetter, you will only be able to scroll to an element that has already been rendered.

scrollAround(index)

Scroll the viewport so that the element at index is visible, but not necessarily at the top. The scrollTo note above also applies to this method.

getVisibleRange() => [firstIndex, lastIndex]

Return the indices of the first and last items that are at all visible in the viewport.

FAQ

What is "ReactList failed to reach a stable state."?

This happens when specifying the uniform type without actually providing uniform size elements. The component attempts to draw only the minimum necessary elements at one time and that minimum element calculation is based off the first element in the list. When the first element does not match the other elements, the calculation will be wrong and the component will never be able to fully resolve the ideal necessary elements.

Why doesn't it work with margins?

The calculations to figure out element positioning and size get significantly more complicated with margins, so they are not supported. Use a transparent border or padding or an element with nested elements to achieve the desired spacing.

Why is there no onScroll event handler?

If you need an onScroll handler, just add the handler to the div wrapping your ReactList component:

<div style={{height: 300, overflow: 'auto'}} onScroll={this.handleScroll}>
  <ReactList ... />
</div>

Development

open docs/index.html
make