TanStack Table: Data-Rich Interfaces für SaaS-Anwendungen

Praxisleitfaden für TanStack Table (React Table) zum Erstellen datenreicher Oberflächen – Sorting, Filtering, Pagination, Zeilenauswahl, Spaltenoptionen und serverseitige Datenintegration für SaaS-Applikationen.

Sam Rivera
Sam Rivera
11. Juni 202613 min read
Auch verfügbar auf:English · 中文

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:

FunktionTanStack TableAG GridMUI Data GridCustom
Bundle-Größe~18 KB~500 KB~120 KBVariiert
Headless✅❌❌✅
TypeScript✅ VollständigTeilweiseTeilweiseManuell
Virtual Scrolling✅✅✅Komplex
Spaltenbreite anpassen✅✅✅Komplex
Zeilenauswahl✅✅✅Manuell
Export/CSVManuell (einfach)✅✅Manuell
PreisKostenlos$999+/JahrKostenlosEntwicklungszeit
Custom RenderingVollständigEingeschränktEingeschränktVollständig

Grundlegende Einrichtung

Installieren Sie das Paket mit Ihrem bevorzugten Paketmanager:

bash
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:

typescript
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:

typescript
// 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.

typescript
// 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:

typescript
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:

typescript
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:

typescript
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:

typescript
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:

typescript
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.