Ein Data Grid mit TanStack Table erstellen
Erstellen Sie ein TanStack Table Data Grid mit Sortierung, Filtern, Pagination und Virtualisierung sowie Fixes für Memoisierungsfehler und Endlos-Rerenders.
TanStack Table ist eine headless-Bibliothek: Sie übernimmt die Tabellenlogik (Sortierung, Filterung, Paginierung und Row Models), während Sie 100 % des Markups und des Stylings selbst kontrollieren.
Wer schon einmal einen Nachmittag damit verbracht hat, mit dem Stylesheet eines vorgefertigten Grids zu kämpfen, nur um einen einzelnen Rahmen zu korrigieren, wird diesen Ansatz sofort zu schätzen wissen. Sie erhalten eine praxiserprobte State-Engine und eine leere Leinwand für das DOM – genau das, was Sie brauchen, wenn das Design-System Ihnen gehört.
Dieser Leitfaden richtet ein echtes Data Grid auf dem aktuellen Stable Release ein, @tanstack/react-table v8: eine einzelne Tabelle mit Sortierung, globalem Filter und Paginierung. Anschließend werden die zwei Skalierungsprobleme behandelt, die in der Produktion tatsächlich zum Problem werden: der Memoization-Bug, der unendliche Re-Renders verursacht, sowie Virtualisierung für Tausende von Zeilen.
Wichtige Erkenntnisse
- In TanStack Table v8 wird jeder Header und jede Zelle mit
flexRendergerendert:flexRender(header.column.columnDef.header, header.getContext())für Header undflexRender(cell.column.columnDef.cell, cell.getContext())für Zellen. Die aus v7 bekannten Aufrufecell.render('Cell')undcolumn.render('Header')existieren nicht mehr. - Sortierung, Filterung und Paginierung sind optionale Row Models: Übergeben Sie
getSortedRowModel(),getFilteredRowModel()undgetPaginationRowModel()in einem einzigenuseReactTable-Aufruf – die Reihenfolge spielt dabei keine Rolle. - Der mit Abstand häufigste Bug in TanStack Table ist eine unendliche Re-Render-Schleife, die durch die Übergabe eines neuen
data- odercolumns-Arrays bei jedem Render entsteht. Wrappen Sie beide inuseMemooder definieren Sie sie auf Modulebene. - Ab einigen Tausend clientseitigen Zeilen sollten Sie auf Paginierung verzichten und stattdessen mit dem
useVirtualizer-Hook von@tanstack/react-virtualübertable.getRowModel().rowsvirtualisieren. - Die aktuelle Stable-Version ist v8 (derzeit 8.21.3 unter dem
latest-Tag auf npm). v9 befindet sich seit Juni 2026 in der Beta-Phase – halten Sie Produktionstabellen daher bei v8.
Was „headless” bedeutet und warum das wichtig ist
Headless bedeutet, dass die Bibliothek Verhalten liefert, keine UI. TanStack Table berechnet Row Models und stellt State und Handler bereit, schreibt jedoch nichts in das DOM: v8 hat auf das Ausliefern von Standard-Styles und role-Attributen verzichtet, damit der Kern framework-agnostisch bleiben kann. Der Vorteil ist vollständige Kontrolle. Die Verantwortung liegt darin, dass Sie auch für die Semantik zuständig sind – <table>, <th scope> und alle ARIA-Rollen liegen in Ihrer Hand.
Praktisch bedeutet das: Styling bringen Sie selbst mit – Tailwind-Utility-Klassen, CSS Modules oder Styled Components auf Ihrem eigenen <table>-Markup. Die Bibliothek berührt es nie.
Ein einfaches TanStack Table Grid einrichten
Discover how at OpenReplay.com.
Installieren Sie das Paket und erstellen Sie die Tabelle mit dem useReactTable-Hook. Definieren Sie Spalten mit createColumnHelper für Typinferenz, übergeben Sie Ihre data und columns, und registrieren Sie getCoreRowModel() – das grundlegende Row Model, das Ihre Daten in Zeilen überführt.
npm install @tanstack/react-table
Der Adapter unterstützt jede React-Version von 16.8 bis 19, mit dem Vorbehalt, dass er möglicherweise nicht korrekt mit dem React Compiler funktioniert, der zusammen mit React 19 ausgeliefert wird.
import {
createColumnHelper,
useReactTable,
getCoreRowModel,
flexRender,
} from '@tanstack/react-table'
type User = { firstName: string; lastName: string; age: number }
const columnHelper = createColumnHelper<User>()
const columns = [
columnHelper.accessor('firstName', { header: 'First Name' }),
columnHelper.accessor('lastName', { header: 'Last Name' }),
columnHelper.accessor('age', { header: 'Age' }),
]
Rendern Sie die Tabelle, indem Sie getHeaderGroups(), getRowModel().rows und die getVisibleCells() jeder Zeile durchlaufen. Übergeben Sie die Column-Definition und ihren Kontext an flexRender, damit String-Header, JSX und Komponenten-Zellen korrekt aufgelöst werden. Gemäß dem v8-Migrationsleitfaden ersetzt flexRender die entfernten Aufrufe cell.render('Cell') / column.render('Header').
function DataGrid({ data }: { data: User[] }) {
const table = useReactTable({
data,
columns,
getCoreRowModel: getCoreRowModel(),
})
return (
<table>
<thead>
{table.getHeaderGroups().map(hg => (
<tr key={hg.id}>
{hg.headers.map(header => (
<th key={header.id}>
{header.isPlaceholder
? null
: flexRender(header.column.columnDef.header, header.getContext())}
</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>
)
}
Sortierung hinzufügen
Aktivieren Sie die Sortierung, indem Sie getSortedRowModel() hinzufügen, den sorting-State mit onSortingChange verwalten und jeden Header mit header.column.getToggleSortingHandler() verknüpfen. Verwenden Sie header.column.getIsSorted() (das 'asc', 'desc' oder false zurückgibt), um den Richtungsindikator anzuzeigen.
const [sorting, setSorting] = useState<SortingState>([])
const table = useReactTable({
data,
columns,
state: { sorting },
onSortingChange: setSorting,
getCoreRowModel: getCoreRowModel(),
getSortedRowModel: getSortedRowModel(),
})
<th onClick={header.column.getToggleSortingHandler()} style={{ cursor: 'pointer' }}>
{flexRender(header.column.columnDef.header, header.getContext())}
{{ asc: ' ↑', desc: ' ↓' }[header.column.getIsSorted() as string] ?? ''}
</th>
Filterung und globale Suche
Die globale Suche besteht aus einem einzigen State-Wert: Fügen Sie getFilteredRowModel() hinzu, speichern Sie einen globalFilter-String im state und aktualisieren Sie ihn mit onGlobalFilterChange. Der Standard-Globalfilter gleicht Teilstrings über alle Spalten hinweg ab.
const [globalFilter, setGlobalFilter] = useState('')
const table = useReactTable({
data,
columns,
state: { sorting, globalFilter },
onSortingChange: setSorting,
onGlobalFilterChange: setGlobalFilter,
getCoreRowModel: getCoreRowModel(),
getSortedRowModel: getSortedRowModel(),
getFilteredRowModel: getFilteredRowModel(),
})
// <input value={globalFilter} onChange={e => setGlobalFilter(e.target.value)} />
Spaltenspezifische Filter verwenden dasselbe Row Model und werden über column.getFilterValue() und column.setFilterValue() aus einem Header-Eingabefeld gesteuert. Fuzzy-Matching ist verfügbar, erfordert jedoch das separate Hilfspaket @tanstack/match-sorter-utils als benutzerdefinierte filterFn. Greifen Sie darauf zurück, wenn Teilstring-Matching nicht ausreicht.
Paginierung
Fügen Sie getPaginationRowModel() hinzu und steuern Sie es mit dem State pageIndex/pageSize. Sichern Sie dann Ihre Vor-/Zurück-Schaltflächen mit table.getCanNextPage() und table.getCanPreviousPage() ab. Die Reihenfolge der Row-Model-Registrierung spielt in v8 keine Rolle, sodass sich diese frei mit Sortierung und Filterung im selben Hook-Aufruf kombinieren lassen.
const [pagination, setPagination] = useState({ pageIndex: 0, pageSize: 10 })
const table = useReactTable({
data,
columns,
state: { sorting, globalFilter, pagination },
onSortingChange: setSorting,
onGlobalFilterChange: setGlobalFilter,
onPaginationChange: setPagination,
getCoreRowModel: getCoreRowModel(),
getSortedRowModel: getSortedRowModel(),
getFilteredRowModel: getFilteredRowModel(),
getPaginationRowModel: getPaginationRowModel(),
})
<button onClick={() => table.previousPage()} disabled={!table.getCanPreviousPage()}>
Previous
</button>
<button onClick={() => table.nextPage()} disabled={!table.getCanNextPage()}>
Next
</button>
Damit haben Sie ein vollständiges, funktionsfähiges Grid: ein einziger useReactTable-Aufruf, der Sortierung, Filterung und Paginierung gemeinsam abbildet.
Skalierung: der Memoization-Fallstrick, Virtualisierung und benutzerdefinierte Zellen
Der mit Abstand häufigste Bug in TanStack Table ist eine unendliche Re-Render-Schleife, die durch die Übergabe eines neuen data- oder columns-Arrays bei jedem Render entsteht. Wenn const columns = [...] oder const data = [...] im Komponentenkörper deklariert wird, entsteht bei jedem Durchlauf eine neue Referenz. TanStack Table erkennt die Identitätsänderung, berechnet die Row Models neu, löst einen Re-Render aus – und der Zyklus wiederholt sich. Wrappen Sie beide in useMemo (oder definieren Sie sie auf Modulebene, wie in den früheren Beispielen), damit ihre Referenzen stabil bleiben.
const columns = useMemo(() => [/* ... */], [])
const data = useMemo(() => fetchedRows, [fetchedRows])
Paginieren oder virtualisieren? Paginieren Sie, wenn Seitengrößen begrenzt sind oder Daten seitenweise vom Server abgerufen werden. Virtualisieren Sie, wenn Sie Tausende von clientseitigen Zeilen in einer einzigen, durchgehenden Scroll-Ansicht halten. Rendern Sie in diesem Fall nur die sichtbaren Zeilen mit @tanstack/react-virtual und dem useVirtualizer-Hook über table.getRowModel().rows. Beachten Sie, dass der aktuelle Hook in v3 useVirtualizer heißt und nicht mehr useVirtual.
import { useVirtualizer } from '@tanstack/react-virtual'
const rows = table.getRowModel().rows
const parentRef = useRef<HTMLDivElement>(null)
const rowVirtualizer = useVirtualizer({
count: rows.length,
getScrollElement: () => parentRef.current,
estimateSize: () => 40,
overscan: 10,
})
Anschließend rendern Sie rowVirtualizer.getVirtualItems() innerhalb eines Platzhalters, dessen Größe durch getTotalSize() bestimmt wird – wie in der TanStack Virtual-Dokumentation beschrieben. Dies ist genau das Fehlerbild, das Session Replay gut aufdeckt: Ein nicht-virtualisiertes Grid mit 10.000 Zeilen fühlt sich lokal auf einem Seed-Datensatz problemlos an, aber erst beim Replay eines echten Nutzers, der in der Produktion sortiert oder schnell scrollt, werden Framedrops und Scroll-Ruckler sichtbar.
Benutzerdefinierte Zellen sind schlicht der cell-Renderer der Spalte: Geben Sie ein Status-Badge, ein formatiertes Datum oder ein editierbares Eingabefeld zurück – die Bibliothek bleibt dabei vollständig im Hintergrund, da sie selbst keine UI rendert. Column Pinning, Größenänderung und Neuanordnung werden ebenfalls über eigene State-APIs unterstützt.
Fazit
Sie verfügen nun über ein Grid, das in einem einzigen useReactTable-Aufruf sortiert, filtert und paginiert – ergänzt durch die zwei Maßnahmen, die es performant halten: stabile data/columns-Referenzen und Virtualisierung ab einigen Tausend Zeilen. Entwickeln Sie heute gegen v8, da sich v9 seit Juni 2026 in der Beta-Phase befindet, und greifen Sie auf useVirtualizer zurück, sobald Ihre Zeilenanzahl eine Seite übersteigt.
Häufig gestellte Fragen
Warum rendert meine TanStack Table unendlich oft neu?
Eine unendliche Re-Render-Schleife entsteht fast immer dadurch, dass bei jedem Render ein neues data- oder columns-Array übergeben wird. Wenn const columns = [] oder const data = [] im Komponentenkörper deklariert wird, entsteht bei jedem Durchlauf eine neue Referenz. TanStack Table erkennt die Identitätsänderung, berechnet seine Row Models neu und löst einen weiteren Render aus. Wrappen Sie beide in useMemo oder definieren Sie sie auf Modulebene, damit ihre Referenzen stabil bleiben.
Sollte ich TanStack Table v8 oder v9 verwenden?
Verwenden Sie v8 für die Produktion. Die aktuelle Stable-Version ist v8 (8.21.3 unter dem latest-Tag auf npm), während sich v9 seit Juni 2026 in der Beta-Phase befindet und weiterhin nur als Prerelease-Build unter dem beta-Tag auf npm verfügbar ist. v8 funktioniert mit React 16.8, 17, 18 und 19. Halten Sie Produktionstabellen bei v8 und experimentieren Sie mit v9 nur in nicht-kritischem Code, bis eine stabile Version veröffentlicht wird.
Wann sollte ich eine TanStack Table paginieren statt virtualisieren?
Paginieren Sie, wenn Seitengrößen begrenzt sind oder Daten seitenweise vom Server abgerufen werden – unter Verwendung von getPaginationRowModel und dem State pageIndex/pageSize. Virtualisieren Sie, wenn Sie Tausende von clientseitigen Zeilen in einer einzigen, durchgehenden Scroll-Ansicht halten: Rendern Sie nur sichtbare Zeilen mit dem useVirtualizer-Hook von @tanstack/react-virtual über table.getRowModel().rows. Beide Ansätze schließen sich nicht gegenseitig aus, aber ab einigen Tausend clientseitigen Zeilen in einem einzelnen Scroll-Bereich ist Virtualisierung die richtige Wahl.
Bietet TanStack Table Standard-Styling oder Accessibility-Rollen?
Nein. TanStack Table v8 ist vollständig headless. Es erzeugt kein Markup, kein Styling und keine role-Attribute. Das v8-Rewrite hat all das entfernt, damit der Kern mit jedem Framework funktioniert. Sie sind für das gesamte DOM verantwortlich – semantisches Markup wie table, th mit scope und alle ARIA-Rollen liegen in Ihrer Hand. Gestalten Sie es mit Tailwind, CSS Modules oder Styled Components auf Ihrem eigenen Markup.
Gain Debugging Superpowers
Unleash the power of session replay to reproduce bugs, track slowdowns and uncover frustrations in your app. Get complete visibility into your frontend with OpenReplay — the most advanced open-source session replay tool for developers.
Star on GitHub12k