DEV Community

Miquéias Telles
Miquéias Telles

Posted on

Construindo uma Tabela com Shadcn, Tanstack Table e Virtualização e Infinite Scroll.

Por que essa combinação é poderosa?

A integração de Shadcn UI, Tanstack Table e Tanstack Virtual (com o useWindowVirtualizer) oferece uma abordagem robusta e eficiente para o desenvolvimento de tabelas em aplicações web:

  • Shadcn UI: Fornece componentes de UI de alta qualidade que podem ser facilmente personalizados e integrados ao seu projeto, acelerando o desenvolvimento e assegurando uma experiência visual coesa e acessível.
  • Tanstack Table: Como uma biblioteca headless, foca na lógica da tabela (ordenação, filtragem, paginação), sem impor estilos visuais. Isso te dá total flexibilidade para controlar a apresentação da sua tabela.
  • Tanstack Virtual (com useWindowVirtualizer): Implementa a virtualização de linhas para otimizar o desempenho em listas extensas. O useWindowVirtualizer foca na rolagem da janela do navegador, renderizando apenas os elementos visíveis, resultando em uma rolagem fluida e uso otimizado de recursos.

Diferença entre useVirtualizer e useWindowVirtualizer

Compreender a diferença entre esses dois hooks é fundamental para escolher a ferramenta certa para seu caso de uso:

  • useVirtualizer: É utilizado quando a rolagem da lista ocorre dentro de um elemento HTML específico (como um div com overflow-auto e uma altura definida). Ele monitora a rolagem desse contêiner isolado.
  • useWindowVirtualizer: Monitora a rolagem da janela do navegador (window) inteira. É ideal para layouts onde a tabela ocupa a maior parte da tela e o usuário rola a página para baixo, não um contêiner interno da tabela.

Neste artigo, focaremos no useWindowVirtualizer para demonstrar uma tabela que rola com a página.


Pré-requisitos

Para seguir este tutorial, você precisa de um projeto React (Next.js, Vite, etc.) configurado. Certifique-se também de que o Shadcn UI já está inicializado em seu projeto. Caso contrário, siga as instruções no site oficial do Shadcn UI.


Instalação das dependências

Primeiro, instale as bibliotecas necessárias:

npm install @tanstack/react-table @tanstack/react-virtual
npx shadcn-ui add table
Enter fullscreen mode Exit fullscreen mode

Estrutura dos Dados e Simulação de API

Vamos simular uma API que retorna dados paginados. Para este exemplo, teremos um array de objetos com id, firstName, lastName e email. O scroll infinito será gerenciado buscando mais dados à medida que o usuário rola a página.

// types.ts ou defina diretamente no seu arquivo de componente
export type Person = {
  id: number;
  firstName: string;
  lastName: string;
  email: string;
};

// Função para simular a busca de dados de uma API
export const fetchData = async (page: number, pageSize: number): Promise<{ data: Person[]; total: number }> => {
  return new Promise((resolve) => {
    setTimeout(() => {
      const allData: Person[] = Array.from({ length: 5000 }, (_, i) => ({
        id: i + 1,
        firstName: `Nome${i + 1}`,
        lastName: `Sobrenome${i + 1}`,
        email: `email${i + 1}@example.com`,
      }));

      const start = page * pageSize;
      const end = start + pageSize;
      const data = allData.slice(start, end);

      resolve({ data, total: allData.length });
    }, 500); // Simula um atraso de rede
  });
};
Enter fullscreen mode Exit fullscreen mode

Criando a Tabela com Scroll Infinito

Agora, vamos criar o componente da tabela. Abaixo está o código completo.

// components/InfiniteScrollTableWindow.tsx
import React, { useState, useEffect, useMemo, useRef, useCallback } from 'react';
import {
  ColumnDef,
  flexRender,
  getCoreRowModel,
  useReactTable,
} from '@tanstack/react-table';
import { useWindowVirtualizer } from '@tanstack/react-virtual';
import {
  Table,
  TableBody,
  TableCell,
  TableHead,
  TableHeader,
  TableRow,
} from '@/components/ui/table';

import { fetchData, Person } from '@/types';

interface InfiniteScrollTableProps {
  pageSize?: number;
}

const InfiniteScrollTableWindow: React.FC<InfiniteScrollTableProps> = ({ pageSize = 50 }) => {
  const [data, setData] = useState<Person[]>([]);
  const [totalRows, setTotalRows] = useState(0);
  const [page, setPage] = useState(0);
  const [isLoading, setIsLoading] = useState(false);

  const tableContainerRef = useRef<HTMLTableElement>(null);

  const fetchMoreData = useCallback(async () => {
    if (isLoading || (data.length >= totalRows && totalRows !== 0)) return;

    setIsLoading(true);
    try {
      const result = await fetchData(page, pageSize);
      setData((prevData) => [...prevData, ...result.data]);
      setTotalRows(result.total);
      setPage((prevPage) => prevPage + 1);
    } catch (error) {
      console.error('Erro ao buscar dados:', error);
    } finally {
      setIsLoading(false);
    }
  }, [page, pageSize, isLoading, data.length, totalRows]);

  useEffect(() => {
    fetchMoreData();
  }, []);

  const columns = useMemo<ColumnDef<Person>[]>(
    () => [
      {
        accessorKey: 'id',
        header: 'ID',
        size: 50,
      },
      {
        accessorKey: 'firstName',
        header: 'Primeiro Nome',
        size: 150,
      },
      {
        accessorKey: 'lastName',
        header: 'Sobrenome',
        size: 150,
      },
      {
        accessorKey: 'email',
        header: 'Email',
        size: 250,
      },
    ],
    []
  );

  const table = useReactTable({
    data,
    columns,
    getCoreRowModel: getCoreRowModel(),
  });

  const { rows } = table.getRowModel();

  const rowVirtualizer = useWindowVirtualizer({
    count: rows.length,
    estimateSize: () => 35,
    overscan: 5,
  });

  useEffect(() => {
    const [lastItem] = [...rowVirtualizer.getVirtualItems()].reverse();
    if (!lastItem) {
      return;
    }

    if (
      lastItem.index >= rows.length - 1 &&
      !isLoading &&
      data.length < totalRows
    ) {
      fetchMoreData();
    }
  }, [rowVirtualizer.getVirtualItems(), rows.length, isLoading, data.length, totalRows, fetchMoreData]);


  return (
    <div className="flex flex-col items-center">
      <h1 className="text-2xl font-bold mb-4 mt-8">Tabela de Rolagem Infinita</h1>
      <Table ref={tableContainerRef} style={{ width: '80%', minWidth: '700px' }} className="border rounded-md shadow-lg">
        <TableHeader className="sticky top-0 z-10 bg-white">
          {table.getHeaderGroups().map((headerGroup) => (
            <TableRow key={headerGroup.id}>
              {headerGroup.headers.map((header) => (
                <TableHead key={header.id} style={{ width: header.getSize() }}>
                  {header.isPlaceholder
                    ? null
                    : flexRender(header.column.columnDef.header, header.getContext())}
                </TableHead>
              ))}
            </TableRow>
          ))}
        </TableHeader>
        <TableBody style={{ height: `${rowVirtualizer.getTotalSize()}px`, position: 'relative' }}>
          {rowVirtualizer.getVirtualItems().map((virtualRow) => {
            const row = rows[virtualRow.index];
            return (
              <TableRow
                data-index={virtualRow.index}
                ref={(node) => rowVirtualizer.measureElement(node)}
                key={row.id}
                style={{
                  position: 'absolute',
                  top: 0,
                  left: 0,
                  width: '100%',
                  height: `${virtualRow.size}px`,
                  transform: `translateY(${virtualRow.start}px)`,
                }}
              >
                {row.getVisibleCells().map((cell) => (
                  <TableCell key={cell.id}>
                    {flexRender(cell.column.columnDef.cell, cell.getContext())}
                  </TableCell>
                ))}
              </TableRow>
            );
          })}
        </TableBody>
      </Table>
      {isLoading && (
        <div className="p-4 text-center text-gray-600">
          Carregando mais dados...
        </div>
      )}
      {!isLoading && data.length >= totalRows && totalRows !== 0 && (
        <div className="p-4 text-center text-gray-600">
          Fim dos resultados.
        </div>
      )}
    </div>
  );
};

export default InfiniteScrollTableWindow;
Enter fullscreen mode Exit fullscreen mode

Explicação das Partes Chave

1. Gerenciamento de Estado e Carregamento de Dados

  • data: Armazena as linhas da tabela.
  • totalRows: O número total de linhas disponíveis, provenientes da "API".
  • page: A página atual que está sendo buscada.
  • isLoading: Um boolean para controlar o estado de carregamento e evitar buscas duplicadas.
  • fetchMoreData: Uma função useCallback que simula a busca de dados, adicionando-os ao estado data e atualizando a página. Esta função é a base do nosso scroll infinito, sendo chamada sempre que o usuário se aproxima do final da lista carregada.

2. Definição das Colunas (columns)

  • Utilizamos useMemo para definir as colunas da tabela. Cada coluna é um objeto ColumnDef do Tanstack Table, com accessorKey (a chave do dado) e header (o título da coluna). Uma propriedade size opcional foi adicionada para estimativa de largura.

3. Inicialização do Tanstack Table (useReactTable)

  • useReactTable é inicializado com data e columns.
  • getCoreRowModel: getCoreRowModel(): Essencial para que o Tanstack Table gerencie as linhas e células internamente.

4. Virtualização com Tanstack Virtual (useWindowVirtualizer)

  • count: O número total de itens a serem virtualizados (o número de linhas rows.length).
  • estimateSize: Uma altura estimada para cada linha, crucial para o useWindowVirtualizer calcular o posicionamento e o tamanho total.
  • overscan: Renderiza um número de linhas extras (acima e abaixo da viewport) para uma rolagem mais fluida.

5. Detecção de Rolagem para Carregamento Infinito

  • Um useEffect monitora os itens virtualizados. Quando o último item visível está próximo do final dos dados carregados e ainda há dados para buscar, a função fetchMoreData é acionada. Isso garante que novos dados sejam carregados dinamicamente à medida que o usuário rola, implementando o scroll infinito.

6. Renderização da Tabela

  • Contêiner da Tabela: Um div pai encapsula a tabela, com estilos para largura e centralização. O Table em si tem um ref e estilos para sua aparência.
  • Shadcn Table Components: Componentes como Table, TableHeader, TableBody, TableCell, TableHead, e TableRow do Shadcn UI são usados para estruturar a tabela.
  • Cabeçalho Fixo: O TableHeader possui a classe sticky top-0 z-10 bg-white para fixá-lo no topo da janela durante a rolagem.
  • Corpo Virtualizado: O TableBody tem uma altura dinâmica definida por ${rowVirtualizer.getTotalSize()}px e position: 'relative'.
    • Iteramos sobre rowVirtualizer.getVirtualItems() para renderizar apenas as linhas visíveis.
    • Cada TableRow recebe estilos de position: 'absolute', height, width e transform: translateY(...) para posicioná-la virtualmente.
    • ref={(node) => rowVirtualizer.measureElement(node)}: Permite que o useWindowVirtualizer meça a altura real de cada linha, melhorando a precisão da virtualização.

7. Indicadores de Estado

  • Mensagens de "Carregando mais dados..." ou "Fim dos resultados." são exibidas na parte inferior da tabela para feedback ao usuário.

Como Usar

Para integrar este componente em sua aplicação React, importe-o e utilize-o como qualquer outro componente:

// pages/index.tsx ou algum outro componente que você queira usar
import InfiniteScrollTableWindow from '@/components/InfiniteScrollTableWindow';

export default function HomePage() {
  return (
    <div className="p-4">
      <div style={{ height: '500px', backgroundColor: '#f0f0f0', marginBottom: '20px', display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: '2em' }}>
        Conteúdo antes da tabela!
      </div>
      <InfiniteScrollTableWindow />
      <div style={{ height: '700px', backgroundColor: '#e0e0e0', marginTop: '20px', display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: '2em' }}>
        Conteúdo depois da tabela! Role para ver a mágica!
      </div>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Ao executar sua aplicação, você observará uma rolagem fluida, mesmo com um grande volume de dados, demonstrando a eficácia da virtualização e do scroll infinito.


Próximos Passos

Esta implementação serve como uma base sólida. Você pode expandir as funcionalidades da sua tabela adicionando:

  • Ordenação: Implemente a lógica de ordenação do Tanstack Table.
  • Filtragem: Adicione campos de busca para filtrar dados por coluna ou globalmente.
  • Redimensionamento de Colunas: Permita que os usuários ajustem a largura das colunas.
  • Seleção de Linhas: Inclua checkboxes para a seleção de linhas.

Essa combinação de bibliotecas oferece flexibilidade e desempenho para construir tabelas complexas com facilidade.

Top comments (0)