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. OuseWindowVirtualizerfoca 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 umdivcomoverflow-autoe 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
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
});
};
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;
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: Umbooleanpara controlar o estado de carregamento e evitar buscas duplicadas. -
fetchMoreData: Uma funçãouseCallbackque simula a busca de dados, adicionando-os ao estadodatae 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
useMemopara definir as colunas da tabela. Cada coluna é um objetoColumnDefdo Tanstack Table, comaccessorKey(a chave do dado) eheader(o título da coluna). Uma propriedadesizeopcional foi adicionada para estimativa de largura.
3. Inicialização do Tanstack Table (useReactTable)
-
useReactTableé inicializado comdataecolumns. -
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 linhasrows.length). -
estimateSize: Uma altura estimada para cada linha, crucial para ouseWindowVirtualizercalcular 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
useEffectmonitora 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çãofetchMoreDataé 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
divpai encapsula a tabela, com estilos para largura e centralização. OTableem si tem umrefe estilos para sua aparência. -
Shadcn Table Components: Componentes como
Table,TableHeader,TableBody,TableCell,TableHead, eTableRowdo Shadcn UI são usados para estruturar a tabela. -
Cabeçalho Fixo: O
TableHeaderpossui a classesticky top-0 z-10 bg-whitepara fixá-lo no topo da janela durante a rolagem. -
Corpo Virtualizado: O
TableBodytem uma altura dinâmica definida por${rowVirtualizer.getTotalSize()}pxeposition: 'relative'.- Iteramos sobre
rowVirtualizer.getVirtualItems()para renderizar apenas as linhas visíveis. - Cada
TableRowrecebe estilos deposition: 'absolute',height,widthetransform: translateY(...)para posicioná-la virtualmente. -
ref={(node) => rowVirtualizer.measureElement(node)}: Permite que ouseWindowVirtualizermeça a altura real de cada linha, melhorando a precisão da virtualização.
- Iteramos sobre
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>
);
}
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)