Lists + virtualization cheatsheet.
Keys
{items.map((it) => <Item key={it.id} item={it} />)}
- Use stable IDs.
- NEVER use index for reorderable / inserted lists.
- Index is OK for truly static append-only lists.
Why: React uses keys to match old vs new elements. Mismatched keys → wrong child state.
TanStack Virtual
npm i @tanstack/react-virtual
import { useVirtualizer } from "@tanstack/react-virtual";
function List({ items }: { items: Item[] }) {
const parentRef = useRef<HTMLDivElement>(null);
const v = useVirtualizer({
count: items.length,
getScrollElement: () => parentRef.current,
estimateSize: () => 40, // estimated row height
overscan: 5,
});
return (
<div ref={parentRef} style={{ height: 400, overflow: "auto" }}>
<div style={{ height: v.getTotalSize(), position: "relative" }}>
{v.getVirtualItems().map((row) => (
<div
key={row.key}
style={{
position: "absolute",
top: 0, left: 0, width: "100%",
height: row.size,
transform: `translateY(${row.start}px)`,
}}
>
{items[row.index].name}
</div>
))}
</div>
</div>
);
}
10k items → ~20 DOM nodes.
Dynamic row height
const v = useVirtualizer({
count: items.length,
getScrollElement: () => parentRef.current,
estimateSize: () => 40,
measureElement: (el) => el.getBoundingClientRect().height,
});
// In render: pass measure ref
<div ref={v.measureElement} data-index={row.index} key={row.key} />
Measures actual height per row.
Horizontal list
const v = useVirtualizer({
count,
horizontal: true,
getScrollElement: () => ref.current,
estimateSize: () => 100,
});
Grid
Use useVirtualizer twice (rows + columns) or use useWindowVirtualizer for full-page.
Infinite scroll
const { data, fetchNextPage, hasNextPage } = useInfiniteQuery({ ... });
const all = data?.pages.flatMap(p => p.items) ?? [];
const v = useVirtualizer({
count: hasNextPage ? all.length + 1 : all.length,
getScrollElement: () => ref.current,
estimateSize: () => 40,
});
useEffect(() => {
const last = v.getVirtualItems().at(-1);
if (!last) return;
if (last.index >= all.length - 1 && hasNextPage) fetchNextPage();
}, [v.getVirtualItems(), hasNextPage]);
react-window (older alternative)
npm i react-window
import { FixedSizeList } from "react-window";
<FixedSizeList height={400} itemCount={items.length} itemSize={40} width="100%">
{({ index, style }) => <div style={style}>{items[index].name}</div>}
</FixedSizeList>
Simpler API, less flexible than TanStack Virtual.
IntersectionObserver for sentinel
const sentinelRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (!sentinelRef.current) return;
const io = new IntersectionObserver(([entry]) => {
if (entry.isIntersecting) fetchNextPage();
});
io.observe(sentinelRef.current);
return () => io.disconnect();
}, [fetchNextPage]);
// Bottom of list:
<div ref={sentinelRef} />
Pairs with non-virtualized “load more” lists.
Item memoization
const Item = React.memo(function Item({ item }) {
return <div>{item.name}</div>;
});
For long lists where individual items re-render heavy, prevents subtree re-renders.
List update patterns
// Add
setItems((arr) => [...arr, newItem]);
// Remove
setItems((arr) => arr.filter((x) => x.id !== id));
// Update one
setItems((arr) => arr.map((x) => x.id === id ? { ...x, name } : x));
// Reorder (move idx 2 to idx 5)
setItems((arr) => {
const copy = [...arr];
const [moved] = copy.splice(2, 1);
copy.splice(5, 0, moved);
return copy;
});
When to virtualize
- Lists with 100+ items.
- Slow scrolling or input lag.
- DOM count contributing to perf issues.
Don’t virtualize tiny lists — overhead outweighs benefit.
Avoiding cumulative layout shift
// Reserve space for unloaded items:
<div style={{ minHeight: items.length * 40 }}>
...
</div>
Or use content-visibility: auto CSS for cheap skip:
.list-item { content-visibility: auto; contain-intrinsic-size: 40px; }
Browser-native virtualization-ish.
Common mistakes
- Index keys + reorderable list → mysterious bugs.
- Items not memoized in 1000-row list → laggy scroll.
- Wrong
estimateSize→ jumpy scrollbar. - Inline functions in virtualized item → re-render storms.
- Spread props through many memoized layers → identity changes.
Read this next
If you want my virtualized list + infinite scroll snippets, they’re at rajpoot.dev .
Building something AI-, backend-, or data-heavy and want a second pair of eyes? I do consulting and freelance work — see my projects and ways to reach me at rajpoot.dev .