Virtualizar Listas Largas con react-window
Cómo renderizar listas grandes eficientemente en React usando react-window para virtualización del DOM, incluyendo filas de altura fija y variable y layouts de grid.
Nota para desarrolladores hispanohablantes: Esta guía incluye ejemplos y convenciones de nomenclatura adaptadas a equipos que trabajan en español. Cuando existen diferencias significativas en terminología técnica entre el inglés y el español, se indican explícitamente para facilitar la comunicación en equipos multiculturales.
Overview
react-window renderiza solo las filas visibles de una lista larga, reciclando nodos del DOM mientras el usuario scrollea. En lugar de renderizar 10,000 elementos <div>, renderiza solo los ~20 visibles más algunas filas de overscan. Esto mantiene el DOM pequeño y el scroll suave, incluso con datasets masivos. react-window es un sucesor más ligero y rápido de react-virtualized.
When to Use
- Listas con 1,000+ items donde renderizar todos causa jank
- Tablas con datasets grandes que necesitan scroll suave
- Grid layouts con muchas celdas
- Listas de mensajes de chat con miles de mensajes
- Cualquier lista donde renderizar todos los items aumenta el uso de memoria o el tiempo de render inicial
When NOT to Use
- Listas con menos de 100 items — el overhead de virtualización no vale la pena
- Listas donde los items tienen alturas muy diferentes difíciles de medir — considera
react-virtuosoen su lugar - Listas que necesitan render completo para SEO o print — el contenido virtualizado no está en el DOM hasta que se scrollea
- Cuando necesitas browser find (Ctrl+F) para buscar todos los items — solo los items visibles están en el DOM
Solution
Setup
npm install react-window
Lista de tamaño fijo
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>
);
}
El prop style de react-window incluye position: absolute, top y height — aplícalo al elemento root de la fila. No overridees estos valores.
Lista de tamaño variable
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) {
// Variar altura basado en contenido
const lines = (index % 5) + 1;
return 40 + lines * 24;
}
function VariableList() {
return (
<VariableSizeList
height={600}
width="100%"
itemCount={items.length}
itemSize={getItemSize}
>
{Row}
</VariableSizeList>
);
}
Layout de grid
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>
);
}
Scroll a item específico
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>
);
}
Scroll horizontal
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>
);
}
Scrollbar custom con 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
Usar con 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>
);
}
Usar con React.memo para componentes de fila
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>
);
}
Pasa data vía prop itemData — react-window la pasa a cada fila. Combinado con memo, previene re-renders innecesarios cuando la lista scrollea.
Best Practices
-
For a deeper guide, see Complete Guide to React Performance Optimization.
-
Usa
FixedSizeListcuando todas las filas tienen la misma altura — es más rápido y simple -
Usa
VariableSizeListsolo cuando las alturas varían — debes proveer una funcióngetItemSize -
Aplica el prop
styleal elemento root de la fila — react-window posiciona las filas absolutamente -
Usa
itemDatapara pasar data a las filas en lugar de closures — esto habilita la optimización conReact.memo -
Setea
overscanCounta 3-5 — renderizar algunas filas extra arriba y abajo previene flashes en blanco durante scroll rápido -
Usa
React.memoen componentes de fila — react-window re-renderiza las filas visibles en scroll, memo previene trabajo innecesario -
Llama
resetAfterIndexenVariableSizeListcuando las alturas de items cambian — los tamaños cacheados quedan stale
Common Mistakes
- Overridear el prop
style: react-window seteaposition,top,height— overridear estos rompe el layout. Mergea estilos adicionales:style={{ ...style, borderBottom: "1px solid #ccc" }}. - No usar
itemDatapara data grande: closures sobre arrays grandes causan re-renders. Pasa data víaitemDatay usaReact.memo. - Usar
VariableSizeListcon alturas fijas:FixedSizeListes más rápido y simple cuando las alturas son uniformes. - Olvidar resetear el cache después de cambios de data:
VariableSizeListcachea los tamaños de items. Llamaref.current.resetAfterIndex(0)después de cambios que afectan alturas. - Setear
heighta un valor fijo de píxeles en layouts responsivos: usa un container parent con altura medida, o usaWindowScrollerpara scroll a nivel de página.
FAQ
¿Cuál es la diferencia entre react-window y react-virtualized?
react-window es el sucesor de react-virtualized del mismo autor. Es más pequeño (6KB vs 30KB), más rápido y tiene una API más simple. Usa react-window para proyectos nuevos. Usa react-virtualized solo si necesitas features que no están en react-window (como sorting de tablas).
¿Cómo manejo contenido dinámico que cambia la altura de fila?
Usa VariableSizeList con una función getItemSize. Si las alturas dependen del contenido renderizado, mide con un ResizeObserver y llama resetAfterIndex para actualizar el cache.
¿Puedo usar react-window con TypeScript?
Sí. Instala @types/react-window y tipa el componente de fila:
import { FixedSizeListProps } from "react-window";
const Row: FixedSizeListProps["children"] = ({ index, style }) => (
<div style={style}>Item {index}</div>
);
¿Cómo hago la lista responsiva al ancho del container?
Usa un ResizeObserver para trackear el ancho del container y pásalo a la lista:
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>
);
}
¿react-window funciona con server-side rendering?
No. react-window depende de APIs del browser para scroll y medición. Para SSR, rendera una lista fallback y reemplázala con la lista virtualizada en el client.
Recursos Relacionados
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.