Tips & Tricks (Diperbarui: 2/6/2026)

Virtual Scrolling React dengan Claude Code dan TanStack Virtual

Bangun virtual scrolling dengan Claude Code, TanStack Virtual, tinggi dinamis, aksesibilitas, dan Playwright.

Virtual Scrolling React dengan Claude Code dan TanStack Virtual

Kapan virtual scrolling dibutuhkan

Virtual scrolling berarti hanya merender baris yang terlihat di viewport dan sedikit baris tambahan di atas serta bawahnya. Daftar bisa punya 10000 item, tetapi React tidak perlu memasang 10000 node DOM sekaligus. DOM adalah struktur elemen yang dipakai browser untuk layout, paint, event, dan informasi aksesibilitas. Jika semua baris dipasang, browser tetap membayar biaya untuk baris yang tidak sedang dilihat pengguna.

Claude Code dapat menulis komponen React dengan cepat, tetapi prompt “buat virtual scroll” biasanya menghasilkan demo. Untuk produk nyata, kamu perlu menjelaskan tinggi baris, perilaku keyboard, konteks screen reader, restore posisi setelah kembali dari halaman detail, efek gambar yang load belakangan, risiko SSR, lebar mobile, dan bukti pengujian.

Use case yang paling umum adalah log viewer, daftar pelanggan, riwayat chat, hasil pencarian, dan tabel admin. Log viewer perlu membaca ribuan baris deploy atau error log. Daftar pelanggan di CRM perlu filter, selection, dan kembali ke posisi sebelumnya. Chat history berisi teks panjang, avatar, dan attachment. Search results sering berubah saat filter diganti. Admin table menggabungkan kolom, permission, action, dan status. Jika datanya juga dimuat bertahap dari server, baca infinite scroll. Untuk optimasi render secara umum, hubungkan dengan performance optimization.

Brief untuk Claude Code

Virtual scrolling mudah terlihat benar, tetapi gagal pada detail kecil. Prompt yang baik harus menyebut library, ukuran data, aksesibilitas, lebar mobile, dan verifikasi.

Implementasikan virtualized log viewer dengan React 18 + TypeScript.

Requirements:
- Gunakan @tanstack/react-virtual.
- Dukung lebih dari 10000 rows tanpa memasang semuanya ke DOM.
- Pakai default row height 44px.
- Tambahkan role, aria-label, aria-posinset, dan aria-setsize.
- Layout harus aman di viewport 390px tanpa horizontal page overflow.
- Jelaskan alasan memilih overscan.
- Tambahkan Playwright test untuk scroll dan mobile width.
- Review hasil akhir dengan dokumentasi resmi TanStack Virtual.

Dengan brief seperti ini, Claude Code tidak hanya membuat list cepat, tetapi juga membuat fitur yang bisa direview. Untuk customer list, ganti field dengan nama, plan, status, dan last activity. Untuk search result, pakai title, excerpt, dan tag. Untuk chat, pakai author, body, dan attachment. Acceptance criteria tetap sama.

Fixed-height log viewer

Untuk React,@tanstack/react-virtual adalah pilihan praktis. Library ini headless: ia menghitung virtual items, offset, dan total size, tetapi tidak memaksakan markup atau style. Kamu tetap mengatur HTML, layout, dan accessibility. Referensi resmi ada di TanStack Virtual docs dan Virtualizer API.

npm install @tanstack/react-virtual

Contoh berikut membuat log viewer dengan tinggi tetap. Elemen luar menjadi scroll container, elemen dalam memberi total height, dan baris yang terlihat ditempatkan dengantranslateY.

import { useRef } from "react";
import { useVirtualizer } from "@tanstack/react-virtual";

type LogRow = {
  id: string;
  level: "info" | "warn" | "error";
  message: string;
  createdAt: string;
};

export function VirtualLogViewer({ rows }: { rows: LogRow[] }) {
  const parentRef = useRef<HTMLDivElement>(null);

  const rowVirtualizer = useVirtualizer({
    count: rows.length,
    getScrollElement: () => parentRef.current,
    estimateSize: () => 44,
    overscan: 12,
    getItemKey: (index) => rows[index]?.id ?? index,
  });

  return (
    <section aria-labelledby="log-heading">
      <h2 id="log-heading">Application logs</h2>
      <div
        ref={parentRef}
        data-testid="virtual-log-viewport"
        role="list"
        aria-label={`Application logs, ${rows.length} rows`}
        style={{
          height: 520,
          overflow: "auto",
          border: "1px solid #d4d4d8",
          borderRadius: 6,
        }}
      >
        <div
          style={{
            height: rowVirtualizer.getTotalSize(),
            position: "relative",
            width: "100%",
          }}
        >
          {rowVirtualizer.getVirtualItems().map((virtualRow) => {
            const row = rows[virtualRow.index];
            if (!row) return null;

            return (
              <div
                key={virtualRow.key}
                role="listitem"
                aria-posinset={virtualRow.index + 1}
                aria-setsize={rows.length}
                style={{
                  position: "absolute",
                  top: 0,
                  left: 0,
                  width: "100%",
                  height: `${virtualRow.size}px`,
                  transform: `translateY(${virtualRow.start}px)`,
                  display: "grid",
                  gridTemplateColumns: "92px 72px minmax(0, 1fr)",
                  gap: 12,
                  alignItems: "center",
                  padding: "0 12px",
                  boxSizing: "border-box",
                  borderBottom: "1px solid #eee",
                }}
              >
                <time dateTime={row.createdAt}>{row.createdAt}</time>
                <strong>{row.level.toUpperCase()}</strong>
                <span style={{ overflowWrap: "anywhere" }}>{row.message}</span>
              </div>
            );
          })}
        </div>
      </div>
    </section>
  );
}

overscan menentukan berapa baris tambahan dirender di luar viewport. Terlalu kecil bisa menampilkan celah putih saat scroll cepat. Terlalu besar membuat DOM kembali berat. Untuk log teks, uji kisaran 8 sampai 16. Untuk baris yang berisi avatar, menu, chart, atau syntax highlight, mulai lebih rendah lalu ukur.

Chat history dengan tinggi dinamis

Chat, komentar support, dan log respons AI hampir selalu memiliki tinggi dinamis. Panjang teks, gambar, attachment, terjemahan, dan banner error mengubah tinggi akhir. Gunakan estimasi lalu ukur node yang benar-benar dirender.

import { useRef } from "react";
import { useVirtualizer } from "@tanstack/react-virtual";

type Message = {
  id: string;
  author: string;
  body: string;
  avatarUrl?: string;
};

export function VirtualChatHistory({ messages }: { messages: Message[] }) {
  const parentRef = useRef<HTMLDivElement>(null);

  const virtualizer = useVirtualizer({
    count: messages.length,
    getScrollElement: () => parentRef.current,
    estimateSize: () => 96,
    overscan: 8,
    getItemKey: (index) => messages[index]?.id ?? index,
  });

  return (
    <div
      ref={parentRef}
      role="log"
      aria-label="Chat history"
      style={{ height: 520, overflow: "auto" }}
    >
      <div style={{ height: virtualizer.getTotalSize(), position: "relative" }}>
        {virtualizer.getVirtualItems().map((virtualItem) => {
          const message = messages[virtualItem.index];
          if (!message) return null;

          return (
            <article
              key={virtualItem.key}
              data-index={virtualItem.index}
              ref={virtualizer.measureElement}
              style={{
                position: "absolute",
                top: 0,
                left: 0,
                width: "100%",
                transform: `translateY(${virtualItem.start}px)`,
                padding: "12px 16px",
                boxSizing: "border-box",
              }}
            >
              {message.avatarUrl ? (
                <img
                  src={message.avatarUrl}
                  alt=""
                  width={32}
                  height={32}
                  loading="lazy"
                  onLoad={() => virtualizer.measure()}
                />
              ) : null}
              <p style={{ margin: 0, fontWeight: 700 }}>{message.author}</p>
              <p style={{ margin: "4px 0 0", overflowWrap: "anywhere" }}>
                {message.body}
              </p>
            </article>
          );
        })}
      </div>
    </div>
  );
}

Gambar yang load belakangan sering membuat tinggi berubah. Sediakan width dan height, lalu ukur ulang saat gambar selesai load. Pada chat, tentukan juga apakah pesan baru selalu membawa viewport ke bawah atau tetap mempertahankan posisi user yang sedang membaca pesan lama.

Aksesibilitas dan keyboard

Karena tidak semua baris ada di DOM, list perlu menjelaskan tujuan, total item, posisi saat ini, dan navigasi keyboard. Customer list sebaiknya mendukung arrow keys, PageUp, PageDown, Home, dan End.

import type { KeyboardEvent } from "react";

type KeyboardParams = {
  activeIndex: number;
  rowCount: number;
  setActiveIndex: (index: number) => void;
  scrollToIndex: (index: number) => void;
};

export function handleVirtualListKeyDown(
  event: KeyboardEvent,
  { activeIndex, rowCount, setActiveIndex, scrollToIndex }: KeyboardParams,
) {
  const lastIndex = Math.max(0, rowCount - 1);
  let nextIndex = activeIndex;

  if (event.key === "ArrowDown") nextIndex = Math.min(lastIndex, activeIndex + 1);
  if (event.key === "ArrowUp") nextIndex = Math.max(0, activeIndex - 1);
  if (event.key === "PageDown") nextIndex = Math.min(lastIndex, activeIndex + 10);
  if (event.key === "PageUp") nextIndex = Math.max(0, activeIndex - 10);
  if (event.key === "Home") nextIndex = 0;
  if (event.key === "End") nextIndex = lastIndex;

  if (nextIndex !== activeIndex) {
    event.preventDefault();
    setActiveIndex(nextIndex);
    scrollToIndex(nextIndex);
  }
}

Jangan hanya mengandalkan focus pada baris, karena baris itu bisa unmount saat scroll. Pola yang lebih stabil adalah focus tetap di container dan baris aktif ditunjukkan denganaria-activedescendant. Saat kembali dari detail page, restorescrollTop dengan key yang memasukkan filter dan sorting. Untuk review lebih dalam, baca accessibility guide.

Playwright check dan prompt review

Virtual scroll perlu bukti. Uji mobile width, scroll ke baris yang diketahui, tidak ada horizontal overflow, dan tidak ada console error.

import { expect, test } from "@playwright/test";

test("virtual log viewer scrolls without horizontal overflow", async ({ page }) => {
  const errors: string[] = [];
  page.on("console", (message) => {
    if (message.type() === "error") errors.push(message.text());
  });

  await page.setViewportSize({ width: 390, height: 844 });
  await page.goto("/debug/virtual-log-viewer");

  const viewport = page.getByTestId("virtual-log-viewport");
  await expect(viewport).toBeVisible();

  const before = await viewport.boundingBox();
  await viewport.evaluate((node) => {
    node.scrollTop = 2400;
  });
  await expect(page.getByText("Log #250")).toBeVisible();
  const after = await viewport.boundingBox();

  expect(after?.width).toBe(before?.width);
  expect(await page.evaluate(() => document.documentElement.scrollWidth)).toBeLessThanOrEqual(
    await page.evaluate(() => document.documentElement.clientWidth),
  );
  expect(errors).toEqual([]);
});

Setelah implementasi, minta Claude Code melakukan review kritis.

Review implementasi React virtual scrolling ini.

Periksa:
- Apakah mengikuti API resmi TanStack Virtual.
- Apakah fixed height dan dynamic height dipisahkan.
- Apakah overscan terlalu kecil dan bisa membuat blank gap.
- Apakah role, aria, dan keyboard behavior konsisten.
- Apakah gambar memicu remeasure setelah load.
- Apakah posisi scroll dipulihkan setelah kembali dari detail page.
- Apakah SSR atau hydration bisa mengubah initial height.
- Apakah Playwright menguji mobile width dan baris setelah scroll.

Pitfall, CTA, dan hasil uji

Pitfall utama adalah memperlakukan tinggi dinamis seperti tinggi tetap, overscan terlalu kecil, overscan terlalu besar, lupa keyboard, tidak memberi total count ke screen reader, tidak restore posisi, tidak mengukur gambar setelah load, mismatch SSR, dan teks panjang yang merusak layout mobile. Model mentalnya: scrollTop -> visible range -> overscan -> virtual rows -> translateY -> real measurement.

Jika tim kamu ingin menerapkan pola ini pada repository nyata, Claude Code training and consultation dapat membantu menyusun requirements, prompt, aturanCLAUDE.md, review aksesibilitas, dan bukti Playwright. Referensi resmi tetap TanStack Virtual docs, lalu lanjutkan dengan infinite scroll dan performance optimization.

Saat diuji, fixed-height log viewer mengurangi jumlah baris DOM yang mounted dibandingrows.map penuh. Pada chat dengan tinggi dinamis, masalah muncul di gambar: tanpa ruang yang disediakan dan remeasure, posisi scroll sedikit melompat. Checklist paling berguna adalah menyesuaikanestimateSize dengan data nyata, menguji lebar 390px, scroll ke baris tengah yang diketahui, dan memastikan tidak ada horizontal overflow.

#Claude Code #virtual scrolling #performance #React #windowing
Gratis

PDF gratis: cheatsheet Claude Code

Masukkan email dan unduh satu halaman berisi command, kebiasaan review, dan workflow aman.

Kami menjaga datamu dan tidak mengirim spam.

Masa

Tentang penulis

Masa

Engineer yang berfokus pada workflow Claude Code praktis dan adopsi tim.