Data Table
Construa uma DataTable com TanStack Table, shadcn, ordenação, filtragem e paginação.
Busque em todas as páginas da documentação
Construa uma DataTable com TanStack Table, shadcn, ordenação, filtragem e paginação.
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥
Cartão de receita de referência rápida - pronto para copiar e colar.
npx shadcn@latest add table button input
npm install @tanstack/react-table"use client";
import {
ColumnDef, flexRender, getCoreRowModel,
useReactTable,
} from "@tanstack/react-table";
import {
Table, TableBody, TableCell, TableHead, TableHeader, TableRow,
} from "@/components/ui/table";
interface DataTableProps<TData, TValue> {
columns: ColumnDef<TData, TValue>[];
data: TData[];
}
export function DataTable<TData, TValue>({
columns, data,
}: DataTableProps<TData, TValue>) {
const table = useReactTable({
data, columns, getCoreRowModel: getCoreRowModel(),
});
return (
<div className="rounded-md border">
<Table>
<TableHeader>
{table.getHeaderGroups().map((hg) => (
<TableRow key={hg.id}>
{hg.headers.map((h) => (
<TableHead key={h.id}>
{h.isPlaceholder ? null : flexRender(h.column.columnDef.header, h.getContext())}
</TableHead>
))}
</TableRow>
))}
</TableHeader>
<TableBody>
{table.getRowModel().rows.length ? (
table.getRowModel().rows.map((row) => (
<TableRow key={row.id}>
{row.getVisibleCells().map((cell) => (
<TableCell key={cell.id}>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</TableCell>
))}
</TableRow>
))
) : (
<TableRow>
<TableCell colSpan={columns.length} className="h-24 text-center">
No results.
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</div>
);
}Quando usar isto: Quando você precisar de uma tabela de dados rica em recursos com ordenação, filtragem, paginação ou seleção de linhas.
"use client";
import { useState } from "react";
import {
ColumnDef, flexRender, getCoreRowModel, getSortedRowModel,
getFilteredRowModel, getPaginationRowModel,
SortingState, ColumnFiltersState,
useReactTable,
} from "@tanstack/react-table";
import {
Table, TableBody, TableCell, TableHead, TableHeader, TableRow,
} from "@/components/ui/table";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { ArrowUpDown } from "lucide-react";
// Tipo de dados
type User = {
id: string;
name: string;
email: string;
role: "admin" | "editor" | "viewer";
status: "active" | "inactive";
lastLogin: string;
};
// Dados de exemplo
const users: User[] = [
{ id: "1", name: "Alice Johnson", email: "alice@example.com", role: "admin", status: "active", lastLogin: "2025-12-01" },
{ id: "2", name: "Bob Smith", email: "bob@example.com", role: "editor", status: "active", lastLogin: "2025-11-28" },
{ id: "3", name: "Charlie Brown", email: "charlie@example.com", role: "viewer", status: "inactive", lastLogin: "2025-10-15" },
{ id: "4", name: "Diana Prince", email: "diana@example.com", role: "admin", status: "active", lastLogin: "2025-12-02" },
{ id: "5", name: "Eve Wilson", email: "eve@example.com", role: "editor", status: "inactive", lastLogin: "2025-09-20" },
{ id: "6", name: "Frank Castle", email: "frank@example.com", role: "viewer", status: "active", lastLogin: "2025-11-30" },
];
// Definições de coluna
const columns: ColumnDef<User>[] = [
{
accessorKey: "name",
header: ({ column }) => (
<Button variant="ghost" onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}>
Name <ArrowUpDown className="ml-2 h-4 w-4" />
</Button>
),
},
{
accessorKey: "email",
header: "Email",
},
{
accessorKey: "role",
header: "Role",
cell: ({ row }) => (
<span className="rounded-full bg-gray-100 px-2 py-0.5 text-xs font-medium capitalize">
{row.getValue("role")}
</span>
),
},
{
accessorKey: "status",
header: "Status",
cell: ({ row }) => {
const status = row.getValue("status") as string;
return (
<span className={`rounded-full px-2 py-0.5 text-xs font-medium ${
status === "active" ? "bg-green-100 text-green-700" : "bg-gray-100 text-gray-600"
}`}>
{status}
</span>
);
},
},
{
accessorKey: "lastLogin",
header: ({ column }) => (
<Button variant="ghost" onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}>
Last Login <ArrowUpDown className="ml-2 h-4 w-4" />
</Button>
),
},
];
export function UsersTable() {
const [sorting, setSorting] = useState<SortingState>([]);
const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([]);
const table = useReactTable({
data: users,
columns,
getCoreRowModel: getCoreRowModel(),
getSortedRowModel: getSortedRowModel(),
getFilteredRowModel: getFilteredRowModel(),
getPaginationRowModel: getPaginationRowModel(),
onSortingChange: setSorting,
onColumnFiltersChange: setColumnFilters,
state: { sorting, columnFilters },
initialState: { pagination: { pageSize: 4 } },
});
return (
<div className="space-y-4">
{/* Filter */}
<Input
placeholder="Filter by name..."
value={(table.getColumn("name")?.getFilterValue() as string) ?? ""}
onChange={(e) => table.getColumn("name")?.setFilterValue(e.target.value)}
className="max-w-sm"
/>
{/* Table */}
<div className="rounded-md border">
<Table>
<TableHeader>
{table.getHeaderGroups().map((hg) => (
<TableRow key={hg.id}>
{hg.headers.map((h) => (
<TableHead key={h.id}>
{h.isPlaceholder ? null : flexRender(h.column.columnDef.header, h.getContext())}
</TableHead>
))}
</TableRow>
))}
</TableHeader>
<TableBody>
{table.getRowModel().rows.length ? (
table.getRowModel().rows.map((row) => (
<TableRow key={row.id}>
{row.getVisibleCells().map((cell) => (
<TableCell key={cell.id}>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</TableCell>
))}
</TableRow>
))
) : (
<TableRow>
<TableCell colSpan={columns.length} className="h-24 text-center">
No results.
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</div>
{/* Pagination */}
<div className="flex items-center justify-between">
<p className="text-sm text-gray-500">
Page {table.getState().pagination.pageIndex + 1} of {table.getPageCount()}
</p>
<div className="flex gap-2">
<Button variant="outline" size="sm" onClick={() => table.previousPage()} disabled={!table.getCanPreviousPage()}>
Previous
</Button>
<Button variant="outline" size="sm" onClick={() => table.nextPage()} disabled={!table.getCanNextPage()}>
Next
</Button>
</div>
</div>
</div>
);
}O que isto demonstra:
useReactTable é o hook principal - ele recebe dados, colunas e modelos de featuregetCoreRowModel, getSortedRowModel, etc.) são plugins tree-shakeableaccessorKey), renderizar cabeçalhos e renderizar célulasflexRender renderiza uma definição de coluna (que pode ser uma string, componente ou função)Table do shadcn são wrappers finos em torno de <table>, <thead>, <tr>, <td> com estilo consistenteSeleção de linha com checkboxes:
import { Checkbox } from "@/components/ui/checkbox";
const selectColumn: ColumnDef<User> = {
id: "select",
header: ({ table }) => (
<Checkbox
checked={table.getIsAllPageRowsSelected()}
onCheckedChange={(v) => table.toggleAllPageRowsSelected(!!v)}
/>
),
cell: ({ row }) => (
<Checkbox
checked={row.getIsSelected()}
onCheckedChange={(v) => row.toggleSelected(!!v)}
/>
),
};
// Adicione ao array de colunas
const columns = [selectColumn, ...otherColumns];
// Habilite a seleção de linha em useReactTable
const table = useReactTable({
...config,
enableRowSelection: true,
onRowSelectionChange: setRowSelection,
state: { rowSelection },
});Paginação do lado do servidor:
const table = useReactTable({
data: serverData.rows,
columns,
getCoreRowModel: getCoreRowModel(),
manualPagination: true,
pageCount: serverData.totalPages,
onPaginationChange: (updater) => {
const next = typeof updater === "function"
? updater(pagination)
: updater;
setPagination(next);
fetchPage(next.pageIndex, next.pageSize);
},
state: { pagination },
});Dropdown de ações de linha:
import {
DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { MoreHorizontal } from "lucide-react";
const actionsColumn: ColumnDef<User> = {
id: "actions",
cell: ({ row }) => (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon"><MoreHorizontal className="h-4 w-4" /></Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={() => editUser(row.original)}>Edit</DropdownMenuItem>
<DropdownMenuItem className="text-red-600" onClick={() => deleteUser(row.original.id)}>
Delete
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
),
};// ColumnDef é genérico
const columns: ColumnDef<User>[] = [
{ accessorKey: "name", header: "Name" }, // accessorKey é tipado contra User
];
// row.original é totalmente tipado
cell: ({ row }) => {
const user: User = row.original;
return <span>{user.email}</span>;
};
// row.getValue é tipado por coluna
row.getValue("status"); // tipado por accessorKey
// Componente DataTable genérico reutilizável
interface DataTableProps<TData, TValue> {
columns: ColumnDef<TData, TValue>[];
data: TData[];
}Definições de coluna devem ser estáveis - Definir colunas dentro do componente causa re-renderizações infinitas. Correção: Defina colunas fora do componente ou use useMemo.
accessorKey vs accessorFn - accessorKey é para acesso simples de propriedade; accessorFn é para valores computados ou aninhados. Misturá-los causa problemas de tipo.
Paginação do lado do servidor reinicia no filtro - Ao filtrar, você precisa redefinir para a página 0. Correção: Redefina pageIndex quando os filtros mudarem.
Grandes conjuntos de dados - Ordenação/filtragem do lado do cliente com 10.000+ linhas pode causar lentidão. Correção: Use paginação, ordenação e filtragem do lado do servidor com as opções manual*.
Cabeçalhos fixos - A Tabela shadcn não inclui cabeçalhos fixos por padrão. Correção: Adicione className="sticky top-0 bg-white z-10" a TableHeader.
| Alternativa | Use Quando | Não Use Quando |
|---|---|---|
<table> Simples | Exibir dados pequenos e estáticos sem interatividade | Você precisa de ordenação, filtragem ou paginação |
| AG Grid | Você precisa de recursos semelhantes ao Excel (edição de célula, pivô, gráficos) | Uma tabela mais simples é suficiente |
| React Virtuoso | Você tem milhares de linhas e precisa de virtualização | Dados paginados se encaixam na sua UX |
| Tremor Table | Você usa a biblioteca de dashboard Tremor | Você usa shadcn/ui |
@tanstack/react-table para a lógica da tabela (ordenação, filtragem, paginação)table, button e input do shadcn para a UInpx shadcn@latest add table button input e npm install @tanstack/react-tableflexRender renderiza uma definição de coluna que pode ser uma string, componente React ou funçãoheader: ({ column }) => (
<Button variant="ghost" onClick={() =>
column.toggleSorting(column.getIsSorted() === "asc")
}>
Name <ArrowUpDown className="ml-2 h-4 w-4" />
</Button>
),Adicione também getSortedRowModel: getSortedRowModel() e onSortingChange: setSorting a useReactTable.
useMemomanualPagination: true e pageCount: serverData.totalPagesonPaginationChange para buscar a próxima página do servidorgetPaginationRowModel() pois o servidor cuida do fatiamentoconst selectColumn: ColumnDef<User> = {
id: "select",
header: ({ table }) => (
<Checkbox
checked={table.getIsAllPageRowsSelected()}
onCheckedChange={(v) => table.toggleAllPageRowsSelected(!!v)}
/>
),
cell: ({ row }) => (
<Checkbox
checked={row.getIsSelected()}
onCheckedChange={(v) => row.toggleSelected(!!v)}
/>
),
};accessorKey é para acesso simples e direto de propriedade (por exemplo, "name" mapeia para row.name)accessorFn é para valores computados ou aninhados (por exemplo, (row) => row.address.city)row.original é totalmente tipado como seu tipo de dados (por exemplo, User)ColumnDef<User> se propaga para que row.original.email seja type-safepageIndex quando os filtros mudampageIndex para 0 sempre que os filtros mudaremconst actionsColumn: ColumnDef<User> = {
id: "actions",
cell: ({ row }) => (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon">
<MoreHorizontal className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={() => editUser(row.original)}>Edit</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
),
};interface DataTableProps<TData, TValue> {
columns: ColumnDef<TData, TValue>[];
data: TData[];
}
export function DataTable<TData, TValue>({
columns, data
}: DataTableProps<TData, TValue>) { ... }className="sticky top-0 bg-white z-10" a TableHeaderoverflow-y: autoRevisado por Chris St. John·Última atualização: 16 de jul. de 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥