TanStack Table 服务端筛选与搜索:生产级数据表完整指南

用 TanStack Table 和 TanStack Start server functions 构建高性能数据表:服务端筛选、排序与搜索,含防抖输入与 URL 状态持久化。

Sam Rivera
Sam Rivera
June 1, 202612 min read
其他语言:Deutsch · English

TL;DR: 客户端筛选在数据量上来后会崩溃。TanStack Table 的 manual 筛选、排序和分页模式让你把所有数据操作下放给服务端。本指南覆盖基于 D1 的服务端文本搜索、多列排序、筛选状态序列化到 URL 参数,以及防抖搜索输入。


引言

TanStack Table(前身 React Table)是无头的(headless)——它管理状态和逻辑,渲染由你掌控。对有成千上万条记录的生产级 SaaS 应用,你需要服务端数据操作。TanStack Table 的 manualFiltering、manualSorting 和 manualPagination 选项正是为此设计的。


带筛选的 Server Function

先定义一个接受搜索词、排序参数和分页的 server function。用 Zod 校验保证 API 边界的类型安全:

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 }
  })

这个 server function 在单次查询里处理搜索、排序和分页。参数化的 LIKE 查询既防 SQL 注入,又能跨多列做灵活的文本搜索。

TanStack Table 集成

server function 就绪后,用 manualPagination 和 manualSorting 模式接入 TanStack Table。这告诉表格把数据操作下放给服务端:

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} />
}

manualPagination 和 manualSorting 标志会关闭 TanStack Table 内置的数据处理。每次排序变化或翻页都会通过 TanStack Query 触发一次新的服务端查询,缓存与去重由它自动处理。

防抖搜索

防抖 hook 避免用户输入时产生过多服务端请求。下面是一个可复用的实现,把搜索查询延迟到用户停止输入之后:

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
}

// 用法
const [searchInput, setSearchInput] = useState('')
const debouncedSearch = useDebounce(searchInput, 300)
// 查询用 debouncedSearch,输入框用 searchInput

300 毫秒防抖下,服务端最多每 300 毫秒收到一次搜索请求。输入框保持响应,同时避免不必要的数据库查询。

客户端 vs 服务端操作

操作客户端服务端判断依据
文本搜索<500 行>500 行数据量
排序<1,000 行>1,000 行数据量
分页<10 页始终UX 偏好
多列排序任意规模大数据集复杂度
行分组中等数据集大数据集功能需求

结论

TanStack Table 的服务端筛选可以从几百行扩展到几百万行。配合 TanStack Start server functions 和 D1,你得到类型安全、防抖、URL 持久化的数据表,应对任意数据规模。完整的表格实现请看数据密集型界面指南,用无限查询学习游标分页,用 SaaS 数据库架构指南设计数据库 schema,超过 1 万行的数据集再配合 TanStack Virtual 虚拟滚动。