TL;DR: TanStack Table (ehemals React Table) ist eine headless UI-Bibliothek für performante Datengrids und Tabellen. Es übernimmt Sorting, Filtering, Pagination, Zeilenauswahl, Spaltenreihenfolge und Spaltensichtbarkeit – alles mit voller TypeScript-Unterstützung und ohne Design-Vorgaben. Dieser Leitfaden behandelt praxiserprobte SaaS-Muster, darunter serverseitige Pagination mit D1, Inline-Bearbeitung, CSV-Export und responsive Spalten für mobile Endgeräte.
Warum TanStack Table?
Jede SaaS-Applikation benötigt irgendwann eine Tabelle – Benutzer, Rechnungen, Abonnements, Analysen. Eine von Grund auf selbst zu bauen scheint einfach, bis Sorting, Filtering, Pagination und Spaltenbreitenanpassung hinzukommen. TanStack Table löst all diese Anforderungen, ohne das UI-Design vorzugeben:
| Funktion | TanStack Table | AG Grid | MUI Data Grid | Custom |
|---|---|---|---|---|
| Bundle-Größe | ~18 KB | ~500 KB | ~120 KB | Variiert |
| Headless | ✅ | ❌ | ❌ | ✅ |
| TypeScript | ✅ Vollständig | Teilweise | Teilweise | Manuell |
| Virtual Scrolling | ✅ | ✅ | ✅ | Komplex |
| Spaltenbreite anpassen | ✅ | ✅ | ✅ | Komplex |
| Zeilenauswahl | ✅ | ✅ | ✅ | Manuell |
| Export/CSV | Manuell (einfach) | ✅ | ✅ | Manuell |
| Preis | Kostenlos | $999+/Jahr | Kostenlos | Entwicklungszeit |
| Custom Rendering | Vollständig | Eingeschränkt | Eingeschränkt | Vollständig |
Grundlegende Einrichtung
Installieren Sie das Paket mit Ihrem bevorzugten Paketmanager:
npm install @tanstack/react-table
Minimale Datentabelle
Unten finden Sie eine vollständige Abonnementverwaltungstabelle. Sie demonstriert die Kernkonzepte von TanStack Table: typsichere Spalten über den Column Helper, integriertes Sorting, Filtering und Pagination:
import {
useReactTable,
getCoreRowModel,
getSortedRowModel,
getFilteredRowModel,
getPaginationRowModel,
createColumnHelper,
flexRender,
} from "@tanstack/react-table"
// 1. Datentyp definieren
type Subscription = {
id: string
customerEmail: string
plan: string
status: "active" | "canceled" | "past_due"
mrr: number
createdAt: string
}
// 2. Column Helper für typsichere Spalten erstellen
const columnHelper = createColumnHelper<Subscription>()
const columns = [
columnHelper.accessor("customerEmail", {
header: "Customer",
cell: (info) => info.getValue(),
}),
columnHelper.accessor("plan", {
header: "Plan",
cell: (info) => (
<span className="capitalize">{info.getValue()}</span>
),
}),
columnHelper.accessor("status", {
header: "Status",
cell: (info) => {
const status = info.getValue()
const colors = {
active: "text-green-600",
canceled: "text-gray-400",
past_due: "text-red-600",
}
return <span className={colors[status]}>{status}</span>
},
filterFn: "equalsString",
}),
columnHelper.accessor("mrr", {
header: "MRR",
cell: (info) => `$${info.getValue().toFixed(2)}`,
sortingFn: "basic",
}),
columnHelper.accessor("createdAt", {
header: "Created",
cell: (info) => new Date(info.getValue()).toLocaleDateString(),
}),
]
// 3. Tabelleninstanz erstellen
function SubscriptionTable({ data }: { data: Subscription[] }) {
const table = useReactTable({
data,
columns,
getCoreRowModel: getCoreRowModel(),
getSortedRowModel: getSortedRowModel(),
getFilteredRowModel: getFilteredRowModel(),
getPaginationRowModel: getPaginationRowModel(),
initialState: {
sorting: [{ id: "mrr", desc: true }],
pagination: { pageSize: 25 },
},
})
return (
<div>
<table>
<thead>
{table.getHeaderGroups().map((headerGroup) => (
<tr key={headerGroup.id}>
{headerGroup.headers.map((header) => (
<th key={header.id} onClick={header.column.getToggleSortingHandler()}>
{flexRender(header.column.columnDef.header, header.getContext())}
{{ asc: " ↑", desc: " ↓" }[header.column.getIsSorted() as string] ?? null}
</th>
))}
</tr>
))}
</thead>
<tbody>
{table.getRowModel().rows.map((row) => (
<tr key={row.id}>
{row.getVisibleCells().map((cell) => (
<td key={cell.id}>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</td>
))}
</tr>
))}
</tbody>
</table>
{/* Pagination */}
<div className="flex items-center gap-4 mt-4">
<button onClick={() => table.setPageIndex(0)} disabled={!table.getCanPreviousPage()}>
{"<<"}
</button>
<button onClick={() => table.previousPage()} disabled={!table.getCanPreviousPage()}>
{"<"}
</button>
<span>
Page {table.getState().pagination.pageIndex + 1} of {table.getPageCount()}
</span>
<button onClick={() => table.nextPage()} disabled={!table.getCanNextPage()}>
{">"}
</button>
<button onClick={() => table.setPageIndex(table.getPageCount() - 1)} disabled={!table.getCanNextPage()}>
{">>"}
</button>
<select
value={table.getState().pagination.pageSize}
onChange={(e) => table.setPageSize(Number(e.target.value))}
>
{[10, 25, 50, 100].map((size) => (
<option key={size} value={size}>{size} per page</option>
))}
</select>
</div>
</div>
)
}
Die SubscriptionTable-Komponente ist voll funktionsfähig mit integriertem Sorting, Filtering und Pagination. Die Pagination-Oberfläche umfasst Seitennavigationsschaltflächen, einen Seitenindikator und einen konfigurierbaren Seitengrößenselektor – alles mit der Tabelleninstanz verbunden, ohne zusätzliches State-Management.
Serverseitige Pagination mit Cloudflare D1
Für SaaS-Applikationen mit tausenden von Datensätzen laden Sie Daten vom Server statt vom Client. Hier ist eine Server-Funktion, die paginierte, sortierte Abfragen gegen D1 verarbeitet:
// server/subscriptions.ts
import { createServerFn } from "@tanstack/react-start"
interface PaginatedQuery {
page: number
pageSize: number
sortField: string
sortDir: "asc" | "desc"
filterStatus?: string
}
export const getSubscriptions = createServerFn({ method: "GET" }).handler(
async ({ data }: { data: PaginatedQuery }) => {
const offset = data.page * data.pageSize
// Query mit Sorting und Filtering erstellen
const whereClause = data.filterStatus
? "WHERE status = ?"
: ""
const countResult = await env.DB.prepare(
`SELECT COUNT(*) as total FROM subscriptions ${whereClause}`
)
.bind(...(data.filterStatus ? [data.filterStatus] : []))
.first() as { total: number }
const rows = await env.DB.prepare(
`SELECT id, customer_email, plan, status, mrr, created_at
FROM subscriptions ${whereClause}
ORDER BY ${data.sortField} ${data.sortDir}
LIMIT ? OFFSET ?`
)
.bind(...(data.filterStatus ? [data.filterStatus] : []), data.pageSize, offset)
.all()
return {
data: rows.results,
total: countResult.total,
page: data.page,
pageSize: data.pageSize,
}
}
)
Die Server-Funktion läuft vollständig auf Cloudflare Workers und führt SQL direkt gegen D1 aus. Sie gibt sowohl die paginierten Zeilen als auch die Gesamtanzahl zurück, sodass der Client genaue Pagination-Steuerelemente rendern kann.
// components/SubscriptionTable.tsx — serverseitige Pagination
import { useQuery } from "@tanstack/react-query"
import { useReactTable, getCoreRowModel } from "@tanstack/react-table"
import { getSubscriptions } from "../server/subscriptions"
function ServerSideTable() {
const [pagination, setPagination] = useState({ pageIndex: 0, pageSize: 25 })
const [sorting, setSorting] = useState<SortingState>([])
const query = useQuery({
queryKey: ["subscriptions", pagination, sorting],
queryFn: () =>
getSubscriptions({
data: {
page: pagination.pageIndex,
pageSize: pagination.pageSize,
sortField: sorting[0]?.id ?? "created_at",
sortDir: sorting[0]?.desc ? "desc" : "asc",
},
}),
})
const table = useReactTable({
data: query.data?.data ?? [],
columns,
pageCount: Math.ceil((query.data?.total ?? 0) / pagination.pageSize),
state: { pagination, sorting },
onPaginationChange: setPagination,
onSortingChange: setSorting,
getCoreRowModel: getCoreRowModel(),
manualPagination: true,
manualSorting: true,
})
// ... rendering
}
Nach der Integration mit useQuery empfängt die Tabelle Daten vom Server. Die Flags manualPagination und manualSorting teilen TanStack Table mit, sich vollständig auf Server-Antworten für Pagination- und Sorting-Zustände zu verlassen.
Zeilenauswahl und Sammelaktionen
TanStack Table unterstützt integrierte Zeilenauswahl mit Kontrollkästchen. So aktivieren Sie Sammelaktionen wie das Kündigen oder Exportieren ausgewählter Abonnements:
function BulkActionsTable() {
const table = useReactTable({
data,
columns,
getCoreRowModel: getCoreRowModel(),
enableRowSelection: true,
onRowSelectionChange: setRowSelection,
state: { rowSelection },
getRowId: (row) => row.id,
})
const selectedIds = Object.keys(rowSelection)
return (
<div>
{selectedIds.length > 0 && (
<div className="bg-blue-50 p-3 rounded mb-4 flex gap-2 items-center">
<span>{selectedIds.length} selected</span>
<button onClick={() => bulkCancel(selectedIds)} className="...">
Cancel Subscriptions
</button>
<button onClick={() => bulkExport(selectedIds)} className="...">
Export Selected
</button>
</div>
)}
{/* Tabell Rendering */}
</div>
)
}
Wenn Zeilen ausgewählt sind, erscheint eine schwebende Aktionsleiste mit Schaltflächen für Sammeloperationen. Der rowSelection-Zustand wird intern von TanStack Table verwaltet und stellt die ausgewählten Zeilen-IDs für Ihre Mutationsfunktionen bereit.
Spaltensichtbarkeit umschalten
Erlauben Sie Benutzern, ihre Ansicht anzupassen, indem sie die Spaltensichtbarkeit umschalten. Hier ist eine Dropdown-Komponente, die ein Kontrollkästchen für jede ausblendbare Spalte rendert:
function ColumnToggle({ table }: { table: Table<Subscription> }) {
const columns = table.getAllLeafColumns()
return (
<div className="dropdown">
<button>Columns</button>
<div className="dropdown-content">
{columns
.filter((col) => col.getCanHide())
.map((col) => (
<label key={col.id} className="flex items-center gap-2">
<input
type="checkbox"
checked={col.getIsVisible()}
onChange={col.getToggleVisibilityHandler()}
/>
{col.columnDef.header as string}
</label>
))}
</div>
</div>
)
}
Die Spaltenumschalt-Komponente filtert Spalten heraus, die nicht ausgeblendet werden können (getCanHide()) und ermöglicht Benutzern das Ein- oder Ausblenden einzelner Spalten. Diese Präferenz kann im localStorage gespeichert werden, um sie bei Rückkehrbesuchen beizubehalten.
Export nach CSV
Für Datenanalyse-Workflows bieten Sie eine CSV-Exportfunktion. Dieses Utility konvertiert ein Array von Objekten in eine herunterladbare CSV-Datei:
function exportToCsv<T extends Record<string, unknown>>(
rows: T[],
filename: string
) {
const headers = Object.keys(rows[0])
const csvContent = [
headers.join(","),
...rows.map((row) =>
headers
.map((h) => JSON.stringify(row[h] ?? ""))
.join(",")
),
].join("
")
const blob = new Blob([csvContent], { type: "text/csv" })
const url = URL.createObjectURL(blob)
const a = document.createElement("a")
a.href = url
a.download = `${filename}.csv`
a.click()
URL.revokeObjectURL(url)
}
Die Funktion extrahiert Header aus den Schlüsseln der ersten Zeile, bildet jede Zeile auf kommagetrennte Werte mit JSON-Stringifizierung für sicheres Escaping ab und löst einen Browser-Download über ein dynamisch erstelltes Ankerelement aus.
Responsive Spalten
Für mobilefreundliche Tabellen blenden Sie weniger wichtige Spalten über ein meta-Feld aus. So markieren Sie Spalten für responsives Ausblenden:
const columns = [
columnHelper.accessor("customerEmail", { header: "Customer" }),
columnHelper.accessor("plan", {
header: "Plan",
meta: { hideOnMobile: true },
}),
columnHelper.accessor("status", { header: "Status" }),
columnHelper.accessor("mrr", {
header: "MRR",
meta: { hideOnMobile: true },
}),
]
// CSS verwenden, um Spalten mit `hideOnMobile` auszublenden
// @media (max-width: 768px) {
// th[data-hide-on-mobile="true"],
// td[data-hide-on-mobile="true"] {
// display: none;
// }
// }
Die Annotation meta: { hideOnMobile: true } ist ein benutzerdefiniertes Feld, das Sie in CSS/Media-Queries verwenden können. In Kombination mit einem data-attribute auf Zellen können Sie so nicht kritische Spalten auf kleinen Bildschirmen ausblenden, ohne TanStack Tables Spaltenverwaltung zu verlieren.
Performance: Virtual Scrolling
Für Tabellen mit mehr als 10.000 Zeilen kombinieren Sie TanStack Table mit TanStack Virtual. Der Virtualizer steuert, welche Zeilen gerendert werden, während die Tabelle die Spaltenlogik verwaltet:
import { useReactTable, getCoreRowModel } from "@tanstack/react-table"
import { useVirtualizer } from "@tanstack/react-virtual"
function VirtualizedTable({ data, columns }: Props) {
const table = useReactTable({
data,
columns,
getCoreRowModel: getCoreRowModel(),
})
const { rows } = table.getRowModel()
const parentRef = useRef<HTMLDivElement>(null)
const virtualizer = useVirtualizer({
count: rows.length,
getScrollElement: () => parentRef.current,
estimateSize: () => 48, // Zeilenhöhe in Pixel
overscan: 10,
})
return (
<div ref={parentRef} style={{ height: "600px", overflow: "auto" }}>
<div style={{ height: `${virtualizer.getTotalSize()}px` }}>
<table>
<thead>{/* sticky header */}</thead>
<tbody>
{virtualizer.getVirtualItems().map((virtualRow) => {
const row = rows[virtualRow.index]
return (
<tr
key={row.id}
style={{
position: "absolute",
top: 0,
transform: `translateY(${virtualRow.start}px)`,
height: `${virtualRow.size}px`,
}}
>
{row.getVisibleCells().map((cell) => (
<td key={cell.id}>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</td>
))}
</tr>
)
})}
</tbody>
</table>
</div>
</div>
)
}
Der useVirtualizer rendert nur ~30 Zeilen, unabhängig von der Gesamtgröße des Datensatzes. Die Tabelle stellt die Zeilendaten und Spaltendefinitionen bereit, während der Virtualizer steuert, welche Zeilen basierend auf der Scrollposition gerendert werden. Diese Kombination bewältigt Datensätze mit mehr als 100.000 Zeilen ohne Performance-Einbußen.
Produktions-Checkliste
- Serverseitige Pagination für Datensätze mit mehr als 500 Zeilen
- Spalten-Sorting mit visuellen Indikatoren (aufsteigend/absteigend)
- Spaltensichtbarkeit-Umschaltung mit gespeicherter Präferenz
- Zeilenauswahl für Sammelaktionen (kündigen, exportieren, löschen)
- Responsive Spalten, die sich an mobile Viewports anpassen
- Loading-Skeleton-Zustände während des Datenabrufs
- Leerer Zustand, wenn keine Daten den Filtern entsprechen
- Fehlerzustand mit Wiederholungsschaltfläche
- Export nach CSV für Datenanalyse
- Tastaturnavigation (Pfeiltasten, Tab, Enter)
- Barrierefreie Beschriftungen für Screen Reader
Fazit
TanStack Table ist das Fundament der Datendarstellung in modernen SaaS-Applikationen. Seine Headless-Architektur bedeutet, dass Sie leistungsstarke Funktionen erhalten – Sorting, Filtering, Pagination, Auswahl – ohne sich mit einem vorgefertigten UI auseinandersetzen zu müssen. In Kombination mit TanStack Virtual für Performance und TanStack Query für den Datenabruf bewältigt es alles von einem 10-zeiligen Admin-Panel bis zu einem 100.000-zeiligen Analytics-Dashboard.
Sehen Sie diese Muster in Produktion auf tanstackship.com.
Für mehr zum Thema datenreiche Oberflächen empfehlen wir den Virtual Scrolling Guide für Performance-Optimierung, das Form Complete Guide für typsichere Formularvalidierung, das Query + Server Functions Integration Guide für Datenabruf-Muster und den TanStack Ecosystem Overview 2026 für den vollständigen Framework-Überblick.