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.
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.
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.
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>
);
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>
);
};
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>
);
};
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>
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.| Feature | react-window | react-virtualized | react-virtual | react-infinite |
|---|---|---|---|---|
| API Style | Component | Component | Hook | Component |
| Bundle Size | Small (~13KB) | Large (~90KB) | Small | Medium |
| Variable Size | Yes (Manual notify) | Yes (CellMeasurer) | Yes (Auto measure) | No |
| Infinite Load | Via Loader Package | Built-in Component | Manual | Built-in Prop |
| Status | â Active | â ī¸ Maintenance | â Active | â Legacy |
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.
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.
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.
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.
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.
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.
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.
A browser-ready efficient scrolling container based on UITableView.
React Infinite 0.7.1 only supports React 0.14 and above. Please pin your package to 0.6.0 for React 0.13 support.
When a long list of DOM elements are placed in a scrollable container, all of them are kept in the DOM even when they are out the user's view. This is highly inefficient, especially in cases when scrolling lists can be tens or hundreds of thousands of items long. React Infinite solves this by rendering only DOM nodes that the user is able to see or might soon see. Other DOM nodes are clustered and rendered as a single blank node.
The relevant files are dist/react-infinite.js and dist/react-infinite.min.js. You must have React available as a global variable named React on the window. Including either file, through concatenation or a script tag, will produce a global variable named Infinite representing the component.
React Infinite uses a Universal Module Definition so you can use it in NPM as well. npm install this package and
var Infinite = require('react-infinite');
If you want to use the source with Browserify, the ES5-compiled source is directly requirable from the /build folder off NPM.
Otherwise, you can follow the instructions for NPM.
To use React Infinite with a list of elements you want to make scrollable, provide them to React Infinite as children.
<Infinite containerHeight={200} elementHeight={40}>
<div className="one"/>
<div className="two"/>
<div className="three"/>
</Infinite>
If not all of the children have the same height, you must provide an array of integers to the elementHeight prop instead.
<Infinite containerHeight={200} elementHeight={[111, 252, 143]}>
<div className="111-px"/>
<div className="252-px"/>
<div className="143-px"/>
</Infinite>
useWindowAsScrollContainer mode)To use the entire window as a scroll container instead of just a single div (thus using window.scrollY instead of a DOM element's scrollTop), add the useWindowAsScrollContainer prop.
<Infinite containerHeight={200} elementHeight={[111, 252, 143]}
useWindowAsScrollContainer>
<div className="111-px"/>
<div className="252-px"/>
<div className="143-px"/>
</Infinite>
displayBottomUpwards mode)React Infinite now supports being used as a chat box, i.e. appended elements appear at the bottom when added, and the loading of the next page occurs when the user scrolls to the top of the container. To do so, simply add the displayBottomUpwards prop. A sample implementation can be consulted for more information - run gulp develop to compile the example files.
<Infinite containerHeight={200} elementHeight={[111, 252, 143]}
displayBottomUpwards>
// insert messages for subsequent pages at this point
<div className="third-latest-chat"/>
<div className="second-latest-chat"/>
<div className="latest-chat-message"/>
</Infinite>
A wrapper div is applied that disables pointer events on the children for a default of 150 milliseconds after the last user scroll action for browsers with inertial scrolling. To configure this, set timeScrollStateLastsForAfterUserScrolls.
Infinite.containerHeightScaleFactor(Number number)This function allows a value to be specified for preloadBatchSize and preloadAdditionalHeight that is a relative to the container height. Please see the documentation for those two configuration options for further information on how to use it.
The children of the <Infinite> element are the components you want to render. This gives you as much flexibility as you need in the presentation of those components. Each child can be a different component if you desire. If you wish to render a set of children not all of which have the same height, you must map each component in the children array to an number representing its height and pass it in as the elementHeight prop.
By default, React Infinite renders a single element with the provided containerHeight, and the list sticks to the top like a regular table. However, you can choose to use the entire window as the scroll container or make React Infinite like a chatbox with the following options. They can be used together if you wish.
useWindowAsScrollContainerDefaults to false. This option allows the window to be used as the scroll container, instead of an arbitrary div, when it is set to true. This means that scroll position is detected by window.scrollY instead of the scrollTop of the div that React Infinite creates. Using this option is a way of achieving smoother scrolling on mobile before the problem is solved for container divs.
displayBottomUpwardsDefaults to false. This allows React Infinite to be used as a chatbox. This means that the scroll is stuck to the bottom by default, and the user scrolls up to the top of the container to load the next page. The children are displayed in the same order.
elementHeightIf each child element has the same height, you can pass a number representing that height as the elementHeight prop. If the children do not all have the same height, you can pass an array which is a map the children to numbers representing their heights to the elementHeight prop.
containerHeightThe height of the scrolling container in pixels. This is a required prop if useWindowAsScrollContainer is not set to true.
preloadBatchSizeDefaults to this.props.containerHeight * 0.5. Imagine the total height of the scrollable divs. Now divide this equally into blocks preloadBatchSize pixels high. Every time the container's scrollTop enters each of these blocks the set of elements rendered in full are those contained within the block and elements that are within preloadAdditionalHeight above and below it.
When working with the window as the scroll container, it is sometimes useful to specify a scale factor relative to the container height as the batch size, so your code does not need to know anything about the window. To do this, use Infinite.containerHeightScaleFactor. So, for example, if you want the preloaded batch size to be twice the container height, write preloadBatchSize={Infinite.containerHeightScaleFactor(2)}.
preloadAdditionalHeightDefaults to this.props.containerHeight. The total height of the area in which elements are rendered in full is height of the current scroll block (see preloadBatchSize) as well as preloadAdditionalHeight above and below it.
When working with the window as the scroll container, it is sometimes useful to specify this relative to the container height. If you want the preloaded additional height to be twice the container height, write preloadAdditionalHeight={Infinite.containerHeightScaleFactor(2)}. Please see preloadBatchSize for more details.
handleScroll(DOMNode node)Defaults to function(){}. A function that is called when the container is scrolled, i.e. when the onScroll event of the infinite scrolling container is fired. The only argument passed to it is the native DOM Node of the scrolling container.
infiniteLoadBeginEdgeOffsetDefaults to undefined, which means that infinite loading is disabled. To disable infinite loading, do not provide this property or set it to undefined.
Regular Mode
When the user reaches this number of pixels from the bottom, the infinite load sequence will be triggered by showing the infinite load spinner delegate and calling the function onInfiniteLoad.
displayBottomUpwards mode
When the user reaches this number of pixels from the top of the container, the infinite load sequence will be triggered by showing the infinite loading spinner delegate at the top of the container and calling onInfiniteLoad.
onInfiniteLoad()Defaults to function(){}. This function is called when the scroll exceeds infiniteLoadBeginEdgeOffset. Before this function is called, the infinite loading spinner is automatically turned on. You can set up infinite scrolling with this function like this:
isInfiniteLoading prop to false to hide the loading spinner displayonInfiniteLoad relies heavily on passing props as a means of communication in the style of idiomatic React.
loadingSpinnerDelegateDefaults to <div/>. The element that is provided is used to render the loading view when React Infinite's isInfiniteLoading property is set to true. A React Node is anything that satisfies React.PropTypes.node.
isInfiniteLoadingDefaults to false. This property determines whether the infinite spinner is showing.
timeScrollStateLastsForAfterUserScrollsDefaults to 150 (in milliseconds). On Apple and some other devices, scroll is inertial. This means that the window continues to scroll for several hundred milliseconds after an onScroll event is fired. To prevent janky behavior, we do not want pointer-events to reactivate before the window has finished moving. Setting this parameter causes the Infinite component to think that the user is still scrolling for the specified number of milliseconds after the last onScroll event is received.
classNameAllows a CSS class to be set on the scrollable container.
Code samples are now available in the /examples directory for your perusal. Two examples are provided, one for constant height with infinite loading and another with random variable heights with infinite loading. To generate the files necessary for the examples, execute npm install && gulp build -E. You may need to first install gulp with npm install -g gulp.
To get you started, here is some sample code that implements an infinite scroll with an simulated delay of 2.5 seconds. A live demo of this example is available on our blog.
var createReactClass = require('create-react-class');
var ListItem = createReactClass({
render: function() {
return <div className="infinite-list-item">
List Item {this.props.num}
</div>;
}
});
var InfiniteList = createReactClass({
getInitialState: function() {
return {
elements: this.buildElements(0, 20),
isInfiniteLoading: false
}
},
buildElements: function(start, end) {
var elements = [];
for (var i = start; i < end; i++) {
elements.push(<ListItem key={i} num={i}/>)
}
return elements;
},
handleInfiniteLoad: function() {
var that = this;
this.setState({
isInfiniteLoading: true
});
setTimeout(function() {
var elemLength = that.state.elements.length,
newElements = that.buildElements(elemLength, elemLength + 1000);
that.setState({
isInfiniteLoading: false,
elements: that.state.elements.concat(newElements)
});
}, 2500);
},
elementInfiniteLoad: function() {
return <div className="infinite-list-item">
Loading...
</div>;
},
render: function() {
return <Infinite elementHeight={40}
containerHeight={250}
infiniteLoadBeginEdgeOffset={200}
onInfiniteLoad={this.handleInfiniteLoad}
loadingSpinnerDelegate={this.elementInfiniteLoad()}
isInfiniteLoading={this.state.isInfiniteLoading}
>
{this.state.elements}
</Infinite>;
}
});
ReactDOM.render(<InfiniteList/>, document.getElementById('react-example-one'));
SeatGeek also currently uses React Infinite in production on our event pages; because we only have pages for events in the future, a link would not be appropriate. To see one, head to one of our team pages for the New York Giants, or the New York Mets, or the New York Knicks, and click on the green button for an event to see them in action in the Omnibox.

Useful notes for how to contribute are available.