Claude Code for react-window: Virtualized Lists and Grids — Claude Skills 360 Blog
Blog / Frontend / Claude Code for react-window: Virtualized Lists and Grids
Frontend

Claude Code for react-window: Virtualized Lists and Grids

Published: June 19, 2027
Read time: 6 min read
By: Claude Skills 360

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.

Keep Reading

Frontend

Claude Code for Chart.js Advanced: Custom Plugins and Mixed Charts

Advanced Chart.js patterns with Claude Code — chart.register() for tree-shaking, mixed chart types combining bar and line, custom plugin API with beforeDraw and afterDatasetsDraw hooks, ScriptableContext for computed colors, ChartDataLabels plugin for value labels, chartjs-plugin-zoom for pan and zoom, custom gradient fills via ctx.createLinearGradient, ChartJS annotation plugin for threshold lines, streaming data with chartjs-plugin-streaming, and react-chartjs-2 with useRef and chart instance.

6 min read Jun 27, 2027
Frontend

Claude Code for Nivo: Rich SVG and Canvas Charts

Build rich data visualizations with Nivo and Claude Code — ResponsiveLine and ResponsiveBar for adaptive charts, ResponsiveHeatMap for matrix data, ResponsiveTreeMap for hierarchal data, ResponsiveSunburst for nested proportions, ResponsiveChord for relationship diagrams, ResponsiveCalendar for activity heat maps, ResponsiveNetwork for force graphs, NivoTheme for consistent styling, tooltip customization with sliceTooltip, and motion config for spring animations.

6 min read Jun 26, 2027
Frontend

Claude Code for Victory Charts: React Native and Web Charts

Build cross-platform charts with Victory and Claude Code — VictoryChart, VictoryLine, VictoryBar, and VictoryScatter for web and React Native, VictoryPie for donut charts, VictoryArea for stacked areas, VictoryAxis for custom axes, VictoryTooltip and VictoryVoronoiContainer for hover tooltips, VictoryBrushContainer for range selection, VictoryZoomContainer for pan and zoom, VictoryLegend for series labels, custom theme with VictoryTheme, and VictoryStack for grouped bars.

6 min read Jun 25, 2027

Put these ideas into practice

Claude Skills 360 gives you production-ready skills for everything in this article — and 2,350+ more. Start free or go all-in.

Back to Blog

Get 360 skills free