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.
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.
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-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 versatile infinite scroll React component.
bower install react-list
# or
npm install react-list
ReactList depends on React.
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>
);
}
}
y)The axis that this list scrolls on.
An index to scroll to after mounting.
A function that receives an index and a key and returns the content to be rendered for the item at that index.
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.
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.
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.
0)The number of items in the list.
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.
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.
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.
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.
100)The number of pixels to buffer at the beginning and end of the rendered list items.
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.
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.
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.
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.
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.
[firstIndex, lastIndex]Return the indices of the first and last items that are at all visible in the viewport.
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.
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.
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>
open docs/index.html
make