TL;DR: 客户端筛选在数据量上来后会崩溃。TanStack Table 的 manual 筛选、排序和分页模式让你把所有数据操作下放给服务端。本指南覆盖基于 D1 的服务端文本搜索、多列排序、筛选状态序列化到 URL 参数,以及防抖搜索输入。
引言
TanStack Table(前身 React Table)是无头的(headless)——它管理状态和逻辑,渲染由你掌控。对有成千上万条记录的生产级 SaaS 应用,你需要服务端数据操作。TanStack Table 的 manualFiltering、manualSorting 和 manualPagination 选项正是为此设计的。
带筛选的 Server Function
先定义一个接受搜索词、排序参数和分页的 server function。用 Zod 校验保证 API 边界的类型安全:
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。这告诉表格把数据操作下放给服务端:
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 避免用户输入时产生过多服务端请求。下面是一个可复用的实现,把搜索查询延迟到用户停止输入之后:
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 虚拟滚动。