react-window virtualizes large lists and grids by rendering only visible rows — <FixedSizeList height={500} itemCount={items.length} itemSize={60} width="100%"> renders rows on demand. VariableSizeList uses itemSize={(index) => sizes[index]} for dynamic heights. FixedSizeGrid columnCount={cols} columnWidth={200} rowCount={rows} rowHeight={50} creates virtualized spreadsheets. AutoSizer from react-virtualized-auto-sizer makes containers responsive. InfiniteLoader from react-window-infinite-loader handles paginated fetching. React.memo(Row, areEqual) prevents unnecessary row rerenders. listRef.current.scrollToItem(index, "center") scrolls programmatically. itemData prop passes shared data to row renderers. <FixedSizeList overscanCount={5}> pre-renders buffer rows for smooth fast scrolling. useVariableSizeList custom hook manages heights with resetAfterIndex. Claude Code generates virtualized lists, infinite scroll feeds, and data grid tables.
CLAUDE.md for react-window
## react-window Stack
- Version: react-window >= 1.8, react-virtualized-auto-sizer >= 1.0, react-window-infinite-loader >= 1.0
- Fixed list: <FixedSizeList height={500} itemCount={data.length} itemSize={72} width="100%" itemData={data}>{Row}</FixedSizeList>
- Variable: const sizeMap = useRef<Record<number, number>>({}); const getSize = useCallback((i) => sizeMap.current[i] ?? 80, [])
- Memo row: const Row = React.memo(({ index, style, data }) => <div style={style}>{data[index].name}</div>, areEqual)
- AutoSizer: <AutoSizer>{({ width, height }) => <FixedSizeList width={width} height={height} .../>}</AutoSizer>
- Scroll to: const listRef = useRef<FixedSizeList>(null); listRef.current?.scrollToItem(target, "smart")
Fixed Size Virtualized List
// components/lists/VirtualContactList.tsx — high-performance contact list
"use client"
import React, { useRef, useState, useCallback, useMemo } from "react"
import { FixedSizeList, areEqual } from "react-window"
import AutoSizer from "react-virtualized-auto-sizer"
import type { ListChildComponentProps } from "react-window"
type Contact = {
id: string
name: string
email: string
avatar?: string
status: "online" | "offline" | "busy"
lastSeen?: string
}
const STATUS_COLORS: Record<Contact["status"], string> = {
online: "#22c55e",
offline: "#9ca3af",
busy: "#f59e0b",
}
interface RowData {
contacts: Contact[]
selectedId: string | null
onSelect: (contact: Contact) => void
}
// Memoized row — only rerenders when data changes
const ContactRow = React.memo(function ContactRow({ index, style, data }: ListChildComponentProps<RowData>) {
const { contacts, selectedId, onSelect } = data
const contact = contacts[index]
if (!contact) return null
const isSelected = contact.id === selectedId
const initials = contact.name.split(" ").map((n) => n[0]).join("").slice(0, 2).toUpperCase()
return (
<div style={style}>
<button
onClick={() => onSelect(contact)}
className={`w-full h-full flex items-center gap-3 px-4 transition-colors text-left ${
isSelected ? "bg-primary/10" : "hover:bg-muted"
}`}
>
{/* Avatar */}
<div className="relative flex-shrink-0">
{contact.avatar ? (
<img src={contact.avatar} alt={contact.name} className="size-10 rounded-full object-cover" />
) : (
<div className="size-10 rounded-full bg-primary/20 flex items-center justify-center text-xs font-semibold text-primary">
{initials}
</div>
)}
<span
className="absolute bottom-0 right-0 size-2.5 rounded-full border-2 border-background"
style={{ backgroundColor: STATUS_COLORS[contact.status] }}
/>
</div>
{/* Info */}
<div className="min-w-0 flex-1">
<p className={`text-sm font-medium truncate ${isSelected ? "text-primary" : ""}`}>
{contact.name}
</p>
<p className="text-xs text-muted-foreground truncate">{contact.email}</p>
</div>
{/* Status / time */}
<div className="text-right flex-shrink-0">
{contact.lastSeen && (
<p className="text-xs text-muted-foreground">{contact.lastSeen}</p>
)}
</div>
</button>
</div>
)
}, areEqual)
interface VirtualContactListProps {
contacts: Contact[]
onContactSelect?: (contact: Contact) => void
}
export function VirtualContactList({ contacts, onContactSelect }: VirtualContactListProps) {
const listRef = useRef<FixedSizeList<RowData>>(null)
const [selectedId, setSelectedId] = useState<string | null>(null)
const [query, setQuery] = useState("")
const filtered = useMemo(
() => query
? contacts.filter((c) => c.name.toLowerCase().includes(query.toLowerCase()) || c.email.toLowerCase().includes(query.toLowerCase()))
: contacts,
[contacts, query],
)
const handleSelect = useCallback((contact: Contact) => {
setSelectedId(contact.id)
onContactSelect?.(contact)
}, [onContactSelect])
const scrollToTop = useCallback(() => {
listRef.current?.scrollToItem(0, "start")
}, [])
// itemData is stable reference-wise when contacts/selection doesn't change
const itemData = useMemo<RowData>(
() => ({ contacts: filtered, selectedId, onSelect: handleSelect }),
[filtered, selectedId, handleSelect],
)
return (
<div className="flex flex-col h-full border rounded-xl overflow-hidden bg-card">
{/* Search bar */}
<div className="p-3 border-b">
<input
type="search"
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder={`Search ${contacts.length.toLocaleString()} contacts...`}
className="w-full px-3 py-2 text-sm rounded-lg border bg-background focus:outline-none focus:ring-2 focus:ring-ring"
/>
</div>
{/* Header stats */}
<div className="px-4 py-2 border-b flex items-center justify-between">
<p className="text-xs text-muted-foreground">
{filtered.length.toLocaleString()} {query ? "results" : "contacts"}
</p>
{filtered.length > 20 && (
<button onClick={scrollToTop} className="text-xs text-primary hover:underline">
↑ Top
</button>
)}
</div>
{/* Virtualized list */}
<div className="flex-1">
<AutoSizer>
{({ height, width }) => (
<FixedSizeList
ref={listRef}
height={height}
width={width}
itemCount={filtered.length}
itemSize={68}
itemData={itemData}
overscanCount={8}
>
{ContactRow}
</FixedSizeList>
)}
</AutoSizer>
</div>
</div>
)
}
Variable Size List with InfiniteLoader
// components/lists/InfinitePostFeed.tsx — variable-height infinite scroll
"use client"
import React, { useRef, useState, useCallback, useEffect } from "react"
import { VariableSizeList, areEqual } from "react-window"
import InfiniteLoader from "react-window-infinite-loader"
import AutoSizer from "react-virtualized-auto-sizer"
import type { ListChildComponentProps } from "react-window"
type Post = {
id: string
title: string
excerpt: string
author: string
publishedAt: string
tags: string[]
readTime: number
image?: string
}
const ITEM_HEIGHT_BASE = 120
const IMAGE_EXTRA = 180
interface RowData {
posts: (Post | undefined)[]
isLoading: (index: number) => boolean
}
const PostRow = React.memo(function PostRow({ index, style, data }: ListChildComponentProps<RowData>) {
const { posts, isLoading } = data
const post = posts[index]
if (isLoading(index) || !post) {
return (
<div style={style} className="px-4 py-3">
<div className="animate-pulse space-y-2">
<div className="h-4 bg-muted rounded w-3/4" />
<div className="h-3 bg-muted rounded w-full" />
<div className="h-3 bg-muted rounded w-5/6" />
</div>
</div>
)
}
return (
<div style={style} className="px-4 py-3 border-b">
<div className="flex gap-4 h-full">
{post.image && (
<img src={post.image} alt={post.title} className="w-32 h-24 object-cover rounded-lg flex-shrink-0" />
)}
<div className="min-w-0 flex-1">
<h3 className="font-semibold text-sm leading-snug line-clamp-2">{post.title}</h3>
<p className="text-xs text-muted-foreground mt-1 line-clamp-2">{post.excerpt}</p>
<div className="flex items-center gap-3 mt-2">
<span className="text-xs text-muted-foreground">{post.author}</span>
<span className="text-xs text-muted-foreground">·</span>
<span className="text-xs text-muted-foreground">{post.readTime} min read</span>
</div>
<div className="flex gap-1 mt-2 flex-wrap">
{post.tags.slice(0, 3).map((tag) => (
<span key={tag} className="text-xs bg-primary/10 text-primary px-2 py-0.5 rounded-full">
{tag}
</span>
))}
</div>
</div>
</div>
</div>
)
}, areEqual)
interface InfinitePostFeedProps {
fetchPage: (page: number) => Promise<{ posts: Post[]; hasMore: boolean }>
}
export function InfinitePostFeed({ fetchPage }: InfinitePostFeedProps) {
const listRef = useRef<VariableSizeList<RowData>>(null)
const [posts, setPosts] = useState<(Post | undefined)[]>([])
const [hasNextPage, setHasNextPage] = useState(true)
const [isNextPageLoading, setIsNextPageLoading] = useState(false)
const pageRef = useRef(0)
// Heights cache per post
const heightCache = useRef<Record<number, number>>({})
const getItemSize = useCallback((index: number) => {
const post = posts[index]
if (!post) return ITEM_HEIGHT_BASE
return (heightCache.current[index] = post.image ? ITEM_HEIGHT_BASE + IMAGE_EXTRA : ITEM_HEIGHT_BASE)
}, [posts])
const isItemLoaded = useCallback((index: number) => !hasNextPage || index < posts.length, [posts.length, hasNextPage])
const loadMoreItems = useCallback(async () => {
if (isNextPageLoading) return
setIsNextPageLoading(true)
const nextPage = pageRef.current + 1
const { posts: newPosts, hasMore } = await fetchPage(nextPage)
pageRef.current = nextPage
setPosts((prev) => [...prev, ...newPosts])
setHasNextPage(hasMore)
setIsNextPageLoading(false)
// Reset height cache for new items
listRef.current?.resetAfterIndex(Math.max(0, posts.length - 1))
}, [fetchPage, isNextPageLoading, posts.length])
useEffect(() => { loadMoreItems() }, []) // initial load
const itemCount = hasNextPage ? posts.length + 1 : posts.length
const itemData: RowData = {
posts,
isLoading: (i: number) => !isItemLoaded(i),
}
return (
<div className="flex flex-col h-full border rounded-xl overflow-hidden bg-card">
<div className="p-4 border-b">
<h2 className="font-semibold">Posts</h2>
<p className="text-xs text-muted-foreground">{posts.length} loaded</p>
</div>
<div className="flex-1">
<AutoSizer>
{({ height, width }) => (
<InfiniteLoader
isItemLoaded={isItemLoaded}
itemCount={itemCount}
loadMoreItems={loadMoreItems}
threshold={5}
>
{({ onItemsRendered, ref }) => (
<VariableSizeList
ref={(el) => {
;(listRef as any).current = el
ref(el)
}}
height={height}
width={width}
itemCount={itemCount}
itemSize={getItemSize}
itemData={itemData}
onItemsRendered={onItemsRendered}
overscanCount={4}
>
{PostRow}
</VariableSizeList>
)}
</InfiniteLoader>
)}
</AutoSizer>
</div>
</div>
)
}
For the TanStack Virtual alternative when a headless, framework-agnostic virtualizer with no opinionated DOM structure (bring your own JSX), support for horizontal virtualization, and better TypeScript generics is preferred — TanStack Virtual gives the most control while react-window has a smaller API surface and is simpler to integrate with existing scroll containers, see the TanStack Virtual guide. For the Virtual DOM approach without virtualization when item counts are below ~500 items and rendering performance is not a concern — browser DOM handles hundreds of items well, but for tens of thousands of records virtualization is indispensable for 60fps scrolling, see the React performance optimization guide. The Claude Skills 360 bundle includes react-window skill sets covering fixed lists, variable height rows, and infinite loaders. Start with the free tier to try virtualized list generation.