typescript 141 lines · 3 tabs

Virtualized List in React: Render Only Visible Rows on Scroll

Shared by codesnips Jul 2026
3 tabs
import React, { useMemo } from 'react';
import { VirtualList } from './VirtualList';

interface LogEntry {
  id: number;
  level: 'info' | 'warn' | 'error';
  message: string;
}

export function LogViewer({ entries }: { entries: LogEntry[] }) {
  const colors = useMemo(
    () => ({ info: '#94a3b8', warn: '#f59e0b', error: '#ef4444' }),
    []
  );

  return (
    <VirtualList
      items={entries}
      itemHeight={28}
      height={480}
      overscan={5}
      renderItem={(entry) => (
        <div
          style={{
            display: 'flex',
            gap: 8,
            padding: '4px 12px',
            fontFamily: 'monospace',
            color: colors[entry.level],
          }}
        >
          <span>[{entry.level.toUpperCase()}]</span>
          <span>{entry.message}</span>
        </div>
      )}
    />
  );
}
3 files · typescript Explain with highlit

Rendering thousands of DOM rows at once destroys scroll performance: layout, paint, and memory all scale with node count. Virtualization (also called windowing) fixes this by only mounting the rows that intersect the visible viewport plus a small buffer, while a spacer element preserves the correct total scroll height so the scrollbar behaves normally.

The useVirtualList hook encapsulates the math. Given a scrollTop value from the container and a fixed itemHeight, it computes the first visible index as scrollTop / itemHeight and derives how many rows fit in containerHeight. An overscan count extends the window above and below the viewport so fast scrolling does not reveal blank gaps before React can render. The hook returns startIndex, endIndex, the totalHeight used to size the scroll spacer, and an offsetY translate value that positions the rendered slice at its true location.

A key detail is that the visible rows are absolutely offset rather than left in normal flow. The hook exposes offsetY = startIndex * itemHeight, and the component applies it with a transform: translateY, which is compositor-friendly and avoids triggering layout on every scroll frame. State updates are throttled through requestAnimationFrame in handleScroll so scroll events, which can fire many times per frame, collapse into at most one render per paint.

In VirtualList component, the outer div owns the scroll and is given a fixed height with overflow: auto. An inner div of totalHeight reserves the full scrollable space, and the sliced children are rendered inside a translated wrapper. The component is generic over T, taking an items array and a renderItem callback so it stays decoupled from any particular row shape. React.memo on Row prevents re-rendering rows whose props are unchanged.

This approach assumes uniform itemHeight; variable heights require measuring rows and maintaining an offset index, which is significantly more complex. The trade-off is worth it for large, mostly-uniform lists such as logs, chat history, or data tables, where keeping the DOM small is the difference between a smooth 60fps scroll and a janky one.


Related snips

ruby
require "csv"

class PeopleCsvStream
  include Enumerable

  HEADERS = %w[id full_name email signed_up_at plan].freeze

Resilient CSV Export as a Streamed Response

rails performance streaming
by codesnips 3 tabs
ruby
Rails.application.configure do
  config.after_initialize do
    Bullet.enable = true
    Bullet.alert = false
    Bullet.bullet_logger = true
    Bullet.console = true

N+1 query detection with Bullet gem

rails performance activerecord
by Alex Kumar 2 tabs
typescript
export type Settled<R> =
  | { status: 'fulfilled'; value: R }
  | { status: 'rejected'; reason: unknown };

export interface ConcurrencyOptions {
  limit: number;

Simple concurrency limiter for batch operations

node concurrency async
by codesnips 2 tabs
javascript
import { Controller } from "@hotwired/stimulus"

export default class extends Controller {
  static targets = ["form"]
  static values = { delay: { type: Number, default: 250 } }

Debounced live search with Stimulus + Turbo Streams

rails hotwire stimulus
by codesnips 4 tabs
ruby
json.array! @posts do |post|
  json.cache! ['v1', post], expires_in: 1.hour do
    json.id post.id
    json.title post.title
    json.excerpt post.excerpt
    json.published_at post.published_at

Fragment caching for expensive JSON serialization

rails caching performance
by Alex Kumar 1 tab
typescript
import axios, { AxiosError } from 'axios'
import { v4 as uuidv4 } from 'uuid'

const api = axios.create({
  baseURL: import.meta.env.VITE_API_URL || 'http://localhost:3000/api/v1',
  timeout: 15000,

Axios API client with interceptors

react axios api
by Maya Patel 1 tab

Share this code

Here's the card — post it anywhere.

Virtualized List in React: Render Only Visible Rows on Scroll — share card
Link copied