执行契约
- 适用场景
- 表格只持有服务端返回的一页数据,排序与分页必须进入 URL、Query Key 和 API 请求。
- 不要使用
- 数据量小且完整存在客户端时,优先使用内置排序与分页行模型,减少远程状态复杂度。
- 前置检查
- 定义可排序列白名单、零基 pageIndex、pageSize 上限、总 rowCount、稳定 Query Key,以及 URL 与 Table 状态的唯一所有者。
- 验证结果
- 验证排序、翻页、刷新、后退与无结果页;每次状态变化只产生一条正确请求,rowCount 驱动的末页按钮准确禁用。
- 失败模式
- 排序只影响当前页说明误用了 getSortedRowModel;状态完全不动通常是传了 onSortingChange 或 onPaginationChange,却漏掉对应 state 值。
- 安全边界
- 列 id、排序方向、页码和 pageSize 都是外部输入;服务端必须白名单映射列、限制范围、独立鉴权,禁止把列 id 直接拼入 SQL。
先确定状态所有权
Table 负责排序和分页的视图模型,Query Key 表达远程结果身份,API 执行全数据集上的排序与分页。若状态需要分享和前进后退,再由 Router Search Params 成为上层唯一来源;不要同时在 URL 与 useState 各保存一份可漂移副本。
控制状态必须成对连接
每个 onXChange 都必须对应 state.x,否则该状态会冻结。服务端模式不提供客户端排序或分页行模型;manualSorting 与 manualPagination 表示传入 data 已由服务端处理。rowCount 让 Table 根据 pageSize 推导页数。
import { useQuery } from '@tanstack/react-query'
import {
getCoreRowModel, type PaginationState, type SortingState, useReactTable,
} from '@tanstack/react-table'
import { useState } from 'react'
const emptyIssues: Issue[] = []
export function useIssueGrid() {
const [sorting, setSorting] = useState<SortingState>([])
const [pagination, setPagination] = useState<PaginationState>({ pageIndex: 0, pageSize: 20 })
const result = useQuery({
queryKey: ['issues', 'grid', { sorting, pagination }] as const,
queryFn: () => fetchIssuePage({ sorting, pagination }),
})
const table = useReactTable({
columns: issueColumns, data: result.data?.items ?? emptyIssues,
getCoreRowModel: getCoreRowModel(), getRowId: (issue) => issue.id,
manualSorting: true, manualPagination: true, rowCount: result.data?.rowCount ?? 0,
state: { sorting, pagination }, onSortingChange: setSorting,
onPaginationChange: setPagination,
})
return { result, table }
}跨页规则必须保持一致
不要把服务端分页与 getSortedRowModel 混用,否则只会排序当前页。排序或筛选变化后应回到第一页;若 URL 是所有者,就在一次导航中同时更新排序和 pageIndex。Query Key 必须包含影响响应的全部状态,但不要包含每次渲染都变化的无关对象。
服务端映射列而不是执行列 id
客户端列 id 只是外部字符串。服务端把允许的 id 映射到固定数据库字段,限制 pageSize 和 pageIndex,拒绝未知排序方向,并在查询前执行资源级授权。rowCount 也必须来自同一授权范围。