TanStack Table: Server-Side Filtering und Search — Produktions-Datentables

High-Performance-Datentables mit serverseitigem Filtering, Sorting und Search via TanStack Table und TanStack Start Server Functions — inklusive Debounced Input.

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

TL;DR: Client-seitiges Filtering bricht bei Skalierung. TanStack Tables manual-Modi für Filtering, Sorting und Pagination delegieren alle Datenoperationen an den Server. Dieser Guide deckt serverseitige Textsuche mit D1, Multi-Column-Sort, Filter-State-Serialisierung in URL-Parameter und debounced Search Input ab.


Einleitung

TanStack Table (früher React Table) ist headless — es verwaltet State und Logik, während Sie das Rendering kontrollieren. Für Produktions-SaaS-Anwendungen mit tausenden Datensätzen brauchen Sie serverseitige Datenoperationen. TanStack Tables manualFiltering-, manualSorting- und manualPagination-Optionen sind genau dafür gebaut.


Server Function mit Filtering

Definieren Sie zuerst eine Server Function, die Suchquery, Sortierparameter und Pagination akzeptiert. Zod-Validierung stellt Typsicherheit ab der API-Grenze sicher:

tsx
import { createServerFn } from '@tanstack/react-start'
import { z } from 'zod'
import { db } from '../db'

const searchSchema = z.object({
  query: z.string().default(''),
  sortBy: z.string().default('createdAt'),
  sortDir: z.enum(['asc', 'desc']).default('desc'),
  page: z.coerce.number().default(1),
  pageSize: z.coerce.number().default(25),
})

export const listCustomersFn = createServerFn({ method: 'GET' })
  .validator(searchSchema)
  .handler(async ({ data }) => {
    const { query, sortBy, sortDir, page, pageSize } = data
    const offset = (page - 1) * pageSize

    let dbQuery = db.selectFrom('customers').selectAll()

    if (query) {
      dbQuery = dbQuery.where((eb) =>
        eb.or([
          eb('name', 'like', `%${query}%`),
          eb('email', 'like', `%${query}%`),
        ])
      )
    }

    const [{ count }] = await dbQuery.select(db.fn.countAll().as('count')).execute()
    const rows = await dbQuery
      .orderBy(sortBy, sortDir)
      .limit(pageSize)
      .offset(offset)
      .execute()

    return { rows, total: count, page, pageSize }
  })

Die Server Function handhabt Suche, Sortierung und Pagination in einer einzigen Query. Parameterisierte LIKE-Queries verhindern SQL-Injection und ermöglichen zugleich flexible Textsuche über mehrere Spalten.

TanStack Table Integration

Mit der fertigen Server Function verdrahten Sie TanStack Table über die manualPagination- und manualSorting-Modi. Das weist die Tabelle an, Datenoperationen an den Server zu delegieren:

tsx
import { useReactTable, getCoreRowModel, getFilteredRowModel } from '@tanstack/react-table'

function CustomerTable() {
  const [search, setSearch] = useState('')
  const [pagination, setPagination] = useState({ pageIndex: 0, pageSize: 25 })
  const [sorting, setSorting] = useState([{ id: 'createdAt', desc: true }])

  const { data, isFetching } = useQuery({
    queryKey: ['customers', search, pagination, sorting],
    queryFn: () => listCustomersFn({
      data: {
        query: search,
        page: pagination.pageIndex + 1,
        pageSize: pagination.pageSize,
        sortBy: sorting[0]?.id || 'createdAt',
        sortDir: sorting[0]?.desc ? 'desc' : 'asc',
      },
    }),
    placeholderData: keepPreviousData,
  })

  const table = useReactTable({
    data: data?.rows ?? [],
    columns,
    pageCount: data ? Math.ceil(data.total / pagination.pageSize) : -1,
    state: { pagination, sorting },
    onPaginationChange: setPagination,
    onSortingChange: setSorting,
    manualPagination: true,
    manualSorting: true,
    getCoreRowModel: getCoreRowModel(),
  })

  return <TableUI table={table} search={search} onSearchChange={setSearch} />
}

Die manualPagination- und manualSorting-Flags deaktivieren TanStack Tables eingebaute Datenverarbeitung. Jede Sortieränderung oder Seiten-Navigation löst eine neue Server-Query via TanStack Query aus, die Caching und Deduplizierung automatisch übernimmt.

Debounced Search

Ein Debounce-Hook verhindert übermäßige Server-Requests während der Eingabe. Hier eine wiederverwendbare Implementierung, die die Such-Query verzögert, bis der Nutzer aufhört zu tippen:

tsx
function useDebounce<T>(value: T, delay: number): T {
  const [debounced, setDebounced] = useState(value)
  useEffect(() => {
    const timer = setTimeout(() => setDebounced(value), delay)
    return () => clearTimeout(timer)
  }, [value, delay])
  return debounced
}

// Verwendung
const [searchInput, setSearchInput] = useState('')
const debouncedSearch = useDebounce(searchInput, 300)
// Query nutzt debouncedSearch, Input nutzt searchInput

Mit 300 ms Debounce erhält der Server höchstens eine Suchanfrage pro 300 ms Tippen. Das Input bleibt responsiv, unnötige Datenbank-Queries werden vermieden.

Client vs. Server-Operationen

OperationClient-seitigServer-seitigEntscheidung
Textsuche<500 Zeilen>500 ZeilenDatenmenge
Sortierung<1.000 Zeilen>1.000 ZeilenDatenmenge
Pagination<10 SeitenImmerUX-Präferenz
Multi-Column-SortJede GrößeGroße DatensätzeKomplexität
Row GroupingMittlere DatensätzeGroße DatensätzeFeature-Bedarf

Fazit

Serverseitiges Filtering mit TanStack Table skaliert von Hunderten bis zu Millionen Zeilen. Kombiniert mit TanStack Start Server Functions und D1 erhalten Sie typsichere, gedebouncete und URL-persistente Datentables für jede Datenmenge. Für eine vollständige Table-Implementierung sehen Sie sich den Data-Rich-Interfaces-Guide an, lernen Sie Cursor-basierte Pagination mit Infinite Queries, designen Sie Ihr DB-Schema mit dem SaaS Database Architecture Guide und kombinieren Sie Virtual Scrolling mit TanStack Virtual Scrolling für Datensätze über 10.000 Zeilen.