Virtualize Long Lists with react-window
How to render large lists efficiently in React using react-window for DOM virtualization, including fixed and variable height rows and grid layouts.
Note: This guide follows English-language naming conventions and terminology standards common in international development teams. Examples use English identifiers and comments to maximize compatibility across codebases and tooling.
Overview
react-window renders only the visible rows of a long list, recycling DOM nodes as the user scrolls. Instead of rendering 10,000 <div> elements, it renders only the ~20 visible ones plus a few overscan rows. This keeps the DOM small and scrolling smooth, even with massive datasets. react-window is a lighter, faster successor to react-virtualized.
When to Use
- Lists with 1,000+ items where rendering all items causes jank
- Tables with large datasets that need smooth scrolling
- Grid layouts with many cells
- Chat message lists with thousands of messages
- Any list where rendering all items increases memory usage or initial render time
When NOT to Use
- Lists with fewer than 100 items — the overhead of virtualization isn’t worth it
- Lists where items have vastly different heights that are hard to measure — consider
react-virtuosoinstead - Lists that need to be fully rendered for SEO or print — virtualized content isn’t in the DOM until scrolled into view
- When you need browser find (Ctrl+F) to search all items — only visible items are in the DOM
Solution
Setup
npm install react-window
Fixed-size list
import { FixedSizeList } from "react-window";
const items = Array.from({ length: 10000 }, (_, i) => `Item ${i}`);
function Row({ index, style }) {
return (
<div style={style} className="list-row">
{items[index]}
</div>
);
}
function VirtualList() {
return (
<FixedSizeList
height={600}
width="100%"
itemCount={items.length}
itemSize={50}
>
{Row}
</FixedSizeList>
);
}
The style prop from react-window includes position: absolute, top, and height — apply it to the row’s root element. Don’t override these values.
Variable-size list
import { VariableSizeList } from "react-window";
const items = Array.from({ length: 1000 }, (_, i) => ({
id: i,
text: `Item ${i} — ${"x".repeat((i % 5) + 1) * 20}`,
}));
function Row({ index, style }) {
const item = items[index];
return (
<div style={style} className="list-row">
<h4>{item.text}</h4>
</div>
);
}
function getItemSize(index) {
// Vary height based on content
const lines = (index % 5) + 1;
return 40 + lines * 24;
}
function VariableList() {
return (
<VariableSizeList
height={600}
width="100%"
itemCount={items.length}
itemSize={getItemSize}
>
{Row}
</VariableSizeList>
);
}
Grid layout
import { FixedSizeGrid } from "react-window";
const columnCount = 4;
const rowCount = 2500;
function Cell({ columnIndex, rowIndex, style }) {
return (
<div style={style} className="grid-cell">
Row {rowIndex}, Col {columnIndex}
</div>
);
}
function VirtualGrid() {
return (
<FixedSizeGrid
columnCount={columnCount}
rowCount={rowCount}
columnWidth={200}
rowHeight={100}
height={600}
width={800}
>
{Cell}
</FixedSizeGrid>
);
}
Infinite scrolling
import { FixedSizeList } from "react-window";
import { useState, useCallback, useRef } from "react";
function InfiniteList() {
const [items, setItems] = useState(
Array.from({ length: 50 }, (_, i) => `Item ${i}`)
);
const [hasMore, setHasMore] = useState(true);
const listRef = useRef(null);
const loadMore = useCallback(() => {
if (!hasMore) return;
setTimeout(() => {
const newItems = Array.from(
{ length: 50 },
(_, i) => `Item ${items.length + i}`
);
setItems((prev) => [...prev, ...newItems]);
if (items.length + newItems.length >= 10000) {
setHasMore(false);
}
}, 500);
}, [items.length, hasMore]);
function handleScroll({ scrollOffset, scrollDirection }) {
if (listRef.current) {
const { height, itemCount } = listRef.current.props;
const totalHeight = itemCount * 50;
if (scrollOffset + height >= totalHeight - 200 && scrollDirection === "forward") {
loadMore();
}
}
}
function Row({ index, style }) {
if (index === items.length && hasMore) {
return (
<div style={style} className="loading-row">
Loading more...
</div>
);
}
return (
<div style={style} className="list-row">
{items[index]}
</div>
);
}
return (
<FixedSizeList
ref={listRef}
height={600}
width="100%"
itemCount={items.length + (hasMore ? 1 : 0)}
itemSize={50}
onScroll={handleScroll}
>
{Row}
</FixedSizeList>
);
}
With scroll-to-item
import { FixedSizeList } from "react-window";
import { useRef } from "react";
function ListWithScroll() {
const listRef = useRef(null);
function scrollToItem(index) {
listRef.current?.scrollToItem(index, "center");
}
return (
<div>
<button onClick={() => scrollToItem(500)}>Jump to item 500</button>
<FixedSizeList
ref={listRef}
height={600}
width="100%"
itemCount={10000}
itemSize={50}
>
{Row}
</FixedSizeList>
</div>
);
}
Horizontal scrolling
import { FixedSizeList } from "react-window";
function HorizontalList() {
return (
<FixedSizeList
layout="horizontal"
height={100}
width={800}
itemCount={1000}
itemSize={150}
>
{({ index, style }) => (
<div style={style} className="horizontal-item">
Card {index}
</div>
)}
</FixedSizeList>
);
}
Custom scrollbar with react-window-scroller
import { FixedSizeList } from "react-window";
import WindowScroller from "react-window-scroller";
function ScrolledList() {
return (
<WindowScroller>
{({ ref, outerRef, style, onScroll }) => (
<FixedSizeList
ref={ref}
outerRef={outerRef}
style={style}
onScroll={onScroll}
height={window.innerHeight}
width="100%"
itemCount={10000}
itemSize={50}
>
{Row}
</FixedSizeList>
)}
</WindowScroller>
);
}
Variants
Using with data fetching
function DataList({ data, loading, onLoadMore }) {
function Row({ index, style }) {
if (index >= data.length) {
return (
<div style={style} className="loading-row">
{loading ? "Loading..." : "Load more"}
</div>
);
}
return (
<div style={style} className="list-row">
<strong>{data[index].name}</strong>
<span>{data[index].email}</span>
</div>
);
}
function handleItemsRendered({ visibleStopIndex }) {
if (visibleStopIndex >= data.length - 5 && !loading) {
onLoadMore();
}
}
return (
<FixedSizeList
height={600}
width="100%"
itemCount={data.length + 1}
itemSize={60}
onItemsRendered={handleItemsRendered}
>
{Row}
</FixedSizeList>
);
}
Using with React.memo for row components
import { memo } from "react";
const MemoizedRow = memo(function Row({ index, style, data }) {
const item = data[index];
return (
<div style={style} className="list-row">
{item.name} — {item.email}
</div>
);
});
function OptimizedList({ items }) {
return (
<FixedSizeList
height={600}
width="100%"
itemCount={items.length}
itemSize={50}
itemData={items}
>
{MemoizedRow}
</FixedSizeList>
);
}
Pass data via itemData prop — react-window passes it to each row. Combined with memo, this prevents unnecessary re-renders when the list scrolls.
Best Practices
-
For a deeper guide, see Complete Guide to React Performance Optimization.
-
Use
FixedSizeListwhen all rows have the same height — it’s faster and simpler -
Use
VariableSizeListonly when row heights vary — you must provide agetItemSizefunction -
Apply the
styleprop to the row’s root element — react-window positions rows absolutely -
Use
itemDatato pass data to rows instead of closures — this enablesReact.memooptimization -
Set
overscanCountto 3-5 — rendering a few extra rows above and below prevents blank flashes during fast scrolling -
Use
React.memoon row components — react-window re-renders visible rows on scroll, memo prevents unnecessary work -
Call
resetAfterIndexonVariableSizeListwhen item heights change — cached sizes become stale
Common Mistakes
- Overriding the
styleprop: react-window setsposition,top,height— overriding these breaks the layout. Merge additional styles:style={{ ...style, borderBottom: "1px solid #ccc" }}. - Not using
itemDatafor large data: closures over large arrays cause re-renders. Pass data viaitemDataand useReact.memo. - Using
VariableSizeListwith fixed heights:FixedSizeListis faster and simpler when heights are uniform. - Forgetting to reset cache after data changes:
VariableSizeListcaches item sizes. Callref.current.resetAfterIndex(0)after data changes that affect heights. - Setting
heightto a fixed pixel value on responsive layouts: use a parent container with a measured height, or useWindowScrollerfor page-level scrolling.
FAQ
What is the difference between react-window and react-virtualized?
react-window is the successor to react-virtualized by the same author. It’s smaller (6KB vs 30KB), faster, and has a simpler API. Use react-window for new projects. Use react-virtualized only if you need features not in react-window (like table sorting).
How do I handle dynamic content that changes row height?
Use VariableSizeList with a getItemSize function. If heights depend on rendered content, measure with a ResizeObserver and call resetAfterIndex to update the cache.
Can I use react-window with TypeScript?
Yes. Install @types/react-window and type the row component:
import { FixedSizeListProps } from "react-window";
const Row: FixedSizeListProps["children"] = ({ index, style }) => (
<div style={style}>Item {index}</div>
);
How do I make the list responsive to container width?
Use a ResizeObserver to track the container width and pass it to the list:
function ResponsiveList() {
const [width, setWidth] = useState(0);
const containerRef = useRef(null);
useEffect(() => {
if (!containerRef.current) return;
const observer = new ResizeObserver((entries) => {
setWidth(entries[0].contentRect.width);
});
observer.observe(containerRef.current);
return () => observer.disconnect();
}, []);
return (
<div ref={containerRef}>
{width > 0 && (
<FixedSizeList height={600} width={width} itemCount={10000} itemSize={50}>
{Row}
</FixedSizeList>
)}
</div>
);
}
Does react-window work with server-side rendering?
No. react-window relies on browser APIs for scrolling and measurement. For SSR, render a fallback list and replace it with the virtualized list on the client.
Related Resources
When to Use useMemo and useCallback
How and when to use React's useMemo and useCallback hooks for performance optimization, and when they add unnecessary overhead.
RecipeForm Validation with react-hook-form and Zod
How to build type-safe forms in React using react-hook-form with Zod schema validation, including nested fields, async validation, and dynamic fields.
RecipeContainer Queries for Component Responsiveness
How to use CSS container queries for component-level responsive layouts that adapt to their container size instead of the viewport.