Tips & Tricks (Atualizado: 02/06/2026)

Scroll virtual em React com Claude Code e TanStack Virtual

Implemente scroll virtual com Claude Code, TanStack Virtual, altura variável, acessibilidade e Playwright.

Scroll virtual em React com Claude Code e TanStack Virtual

Quando usar scroll virtual

Scroll virtual renderiza apenas as linhas visíveis no viewport e algumas linhas extras antes e depois. A lista pode ter dez mil registros, mas o React não precisa montar dez mil nós DOM de uma vez. DOM é a árvore de elementos que o navegador usa para layout, pintura, eventos e tecnologias assistivas. Quando muitas linhas invisíveis continuam montadas, o custo aparece em memória, scroll, foco e tempo de renderização.

Claude Code ajuda a escrever o componente, mas pedir apenas “faça virtual scroll” costuma gerar uma demo. Em produção, você precisa informar se a linha tem altura fixa ou variável, como lidar com imagem carregada depois, como restaurar a posição ao voltar de uma tela de detalhe, como navegar por teclado, como avisar leitores de tela e como provar que o layout não quebra em 390px de largura.

Os casos mais úteis são visor de logs, lista de clientes, histórico de chat, resultados de busca e tabelas administrativas. Logs precisam mostrar milhares de linhas sem travar. Listas de clientes em CRM precisam manter seleção, filtros e retorno para a posição anterior. Chats misturam mensagens longas, avatares e anexos. Busca redesenha resultados após filtros. Tabelas internas juntam colunas, permissões, ações e estado de seleção. Se o desafio também inclui carregar mais dados do servidor, leia infinite scroll. Para otimização geral, veja performance optimization.

Brief para o Claude Code

Um bom prompt transforma a tarefa em algo revisável. Ele deve incluir biblioteca, escala, acessibilidade, largura mobile e teste.

Implemente um visor de logs virtualizado com React 18 + TypeScript.

Requisitos:
- Usar @tanstack/react-virtual.
- Suportar mais de 10000 linhas sem montar todas no DOM.
- Usar 44px como altura base da linha.
- Adicionar role, aria-label, aria-posinset e aria-setsize.
- Manter o layout correto em 390px sem overflow horizontal da pagina.
- Explicar a escolha de overscan.
- Adicionar teste Playwright para scroll e largura mobile.
- Revisar o resultado com a documentacao oficial do TanStack Virtual.

Esse formato evita que o modelo entregue apenas um exemplo bonito. Para lista de clientes, troque os campos por nome, plano, status e última atividade. Para resultados de busca, use título, resumo e tags. Para chat, use autor, corpo e anexos. O padrão é sempre o mesmo: primeiro defina o risco, depois peça a implementação.

Lista fixa com TanStack Virtual

Em React,@tanstack/react-virtual é uma escolha prática. A biblioteca é headless: ela calcula itens virtuais, offsets e tamanho total, mas não impõe HTML nem CSS. Você controla marcação, layout e acessibilidade. As referências oficiais são TanStack Virtual docs e Virtualizer API.

npm install @tanstack/react-virtual

O exemplo abaixo cria um visor de logs com altura fixa. O elemento externo rola, o interno representa a altura total e cada linha visível é posicionada comtranslateY.

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 define quantas linhas extras são renderizadas fora do viewport. Pouco overscan pode mostrar espaços em branco durante scroll rápido. Muito overscan monta linhas demais e reduz o ganho. Para logs simples, teste entre 8 e 16. Para linhas com avatar, menu, gráfico ou destaque de código, comece menor e meça.

Altura variável em histórico de chat

Chats, comentários de suporte e respostas de IA raramente têm altura fixa. Texto, imagem, anexo, tradução e mensagem de erro mudam o tamanho real da linha. Nesse cenário, use uma estimativa e meça o elemento renderizado.

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>
  );
}

Imagem que carrega depois é uma armadilha comum. Reserve largura e altura, e remeça quando carregar. Em chat, defina também se novas mensagens devem prender a tela no final ou respeitar a posição de quem está lendo histórico antigo.

Acessibilidade e teclado

Como nem todas as linhas existem no DOM, a lista precisa comunicar melhor seu propósito, total, posição atual e atalhos. Uma lista de clientes deve funcionar com setas, PageUp, PageDown, Home e 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);
  }
}

Evite depender do foco direto na linha, porque ela pode desmontar durante o scroll. Um padrão mais estável é manter foco no contêiner e indicar a linha ativa comaria-activedescendant. Ao voltar de uma tela de detalhe, restaurescrollTop com uma chave que inclua filtros e ordenação. Para revisar isso com mais profundidade, veja accessibility guide.

Teste Playwright e revisão

A prova mínima deve cobrir largura mobile, scroll até uma linha conhecida, ausência de overflow horizontal e erros de console.

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([]);
});

Depois, peça uma revisão crítica ao Claude Code.

Revise esta implementacao de scroll virtual em React.

Verifique:
- Se segue a API oficial do TanStack Virtual.
- Se separa altura fixa e altura variavel.
- Se overscan baixo pode criar espacos em branco.
- Se role, aria e teclado estao coerentes.
- Se imagens recalculam altura ao carregar.
- Se a posicao e restaurada ao voltar de detalhes.
- Se SSR ou hydration podem alterar a altura inicial.
- Se Playwright valida largura mobile e linha apos scroll.

Armadilhas, CTA e resultado

As armadilhas principais são tratar altura variável como fixa, usar overscan pequeno demais, usar overscan grande demais, esquecer teclado, não informar total e posição para leitor de tela, não restaurar scroll, não medir imagem após carregar, ignorar diferença de SSR e deixar texto longo quebrar a largura mobile. O modelo mental éscrollTop -> faixa visível -> overscan -> linhas virtuais -> translateY -> medição real.

Se sua equipe quer aplicar isso em um repositório real, a página de treinamento e consultoria Claude Code pode ajudar a montar requisitos, prompts, regrasCLAUDE.md, revisão de acessibilidade e evidência Playwright. Referências úteis: TanStack Virtual docs, infinite scroll e performance optimization.

Ao testar, o visor de logs fixo reduziu muito as linhas montadas em comparação comrows.map. No chat de altura variável, o problema apareceu nas imagens: sem espaço reservado e nova medição, o scroll saltava levemente. A checklist mais útil foi ajustarestimateSize com dados reais, testar 390px, rolar até uma linha conhecida no meio e confirmar que não há overflow horizontal.

#Claude Code #scroll virtual #performance #React #windowing
Grátis

PDF grátis: cheatsheet do Claude Code

Informe seu e-mail e baixe uma página com comandos, hábitos de revisão e workflows seguros.

Cuidamos dos seus dados e não enviamos spam.

Masa

Sobre o autor

Masa

Engenheiro focado em workflows práticos com Claude Code.