Refine v5 使用指南:useTable Hook 与 Ant Design 表格的分页、排序、筛选实战
【免费下载链接】refineA React Framework for building internal tools, admin panels, dashboards & B2B apps with unmatched flexibility.项目地址: https://gitcode.com/GitHub_Trending/re/refine
导读
useTable是 Refine 面向 Ant Design 生态提供的核心表格 Hook,它返回与 Ant Design<Table>为主线,结合@refinedev/antd与@refinedev/core的实际源码,讲解useTable的完整用法、全部可配置属性、返回值语义,以及常见业务场景(关系数据展示、客户端筛选、客户端排序)的解决方案。读完本文,你将能够独立搭建一个支持服务端分页、排序、筛选与可分享 URL 状态的专业后台表格页面。
认识 useTable:基于 useList 的 Ant Design 表格适配层
useTable是@refinedev/antd包提供的 Hook,通过它你可以在不手写任何数据请求逻辑的情况下,获得与 Ant Design<Table>组件兼容的全部属性。排序、筛选、分页等表格核心特性全部内置,无需额外配置。
从源码结构看,useTable是分层架构中的 UI 适配层:
- 数据获取与状态管理核心在
@refinedev/core的useTable中实现,底层通过useList发起数据请求; @refinedev/antd的useTable将 core 层的返回值映射为 Ant Design<Table>所需的tableProps,并额外提供searchFormProps(搜索表单属性)与onSearch(搜索回调)。
这意味着:core 层useTable的所有特性(分页、排序、筛选、实时更新、超时检测等)在 Ant Design 版本中全部可用,同时你还能直接使用 Ant Design<Table>的全部既有功能。
在 packages/antd/src/hooks/table/useTable/useTable.ts 中可以看到,antd 版本的useTable首先调用useTableCore,然后在其返回值之上进行二次封装——例如用 Ant Design 的Form.useForm维护搜索表单状态、用Grid.useBreakpoint()根据响应式断点决定分页条的位置与形态。这正是“UI 适配层”一词的准确含义。
基础用法
在最基础的场景中,useTable会直接返回接口(endpoint)返回的数据,并默认从当前路由 URL 推断resource:
import { useTable } from "@refinedev/antd"; import { Table } from "antd"; const { tableProps } = useTable<IPost>(); <Table {...tableProps} rowKey="id"> <Table.Column dataIndex="id" title="ID" /> <Table.Column dataIndex="title" title="Title" /> </Table>;把tableProps展开到<Table>上,数据源、加载状态、分页配置、排序筛选回调就全部接好了。对应的官方可运行示例位于 examples/table-antd-use-table,其端到端测试见 cypress/e2e/table-antd-use-table。
分页(Pagination)
分页能力由tableProps.pagination开箱即用地提供。它会为<Table>生成分页链接(而不是依赖 React 内部 state),并覆写<Table>默认的pagination.itemRender,让每个页码都渲染为基于路由的链接。
如果开启了syncWithLocation,分页状态还会与 URL 查询参数双向同步(详见后文)。
自定义分页条
当你需要调整<Table>的分页展示时,把tableProps.pagination对象回传给<Table>的pagination属性,然后按需覆盖其中的字段:
const { tableProps } = useTable<IPost>(); <Table {...tableProps} rowKey="id" pagination={{ ...tableProps.pagination, position: ["bottomCenter"], size: "small", }} > {/* ... */} </Table>;实现提示
- 默认情况下分页在服务端完成:每次翻页都会用新的
currentPage与pageSize重新请求接口;- 若要在客户端完成分页,将
pagination.mode设为"client",此时会一次性拉取全部记录再在前端分页;- 设置为
"off"则完全禁用分页,拉取所有记录。
从 useTable.ts 的antdPagination()实现可以看到,分页对象会根据Grid.useBreakpoint()的结果自动调整:小屏设备使用simple模式并将分页条位置改为["bottomCenter"],大屏默认["bottomRight"];total取服务端返回的记录总数,用于计算总页数。分页链接由 paginationLink.tsx 中的PaginationLink组件渲染,它通过useLink()拿到当前路由方案(React Router / Next.js / Remix)对应的Link,从而保证与项目路由系统一致。
排序(Sorting)
要给某列开启排序,只需为对应的<Table.Column>添加sorter属性。开启syncWithLocation后,排序状态同样会与 URL 同步。
排序生效期间,API 请求中使用的字段名取自<Column>的key属性;若列没有设置key,则回退使用dataIndex。当你的dataIndex与后端排序字段名不一致时,这个机制就派上了用场。
多列排序时,sorter属性必须提供multiple值,用来指定该列在排序中的优先级:
<Table.Column dataIndex="title" title="Title" sorter={{ multiple: 1 }} />在 useTable.ts 的onChange回调中可以看到排序与筛选的处理链路:Ant Design 的SorterResult通过mapAntdSorterToCrudSorting映射为 Refine 的CrudSorting,再交给setSorters更新 core 层状态;随后 core 层会将其作为sorters参数传给useList,由数据提供者(data provider)拼接到实际请求中。映射函数定义在 packages/antd/src/definitions/table。
筛选(Filtering)
基于列值的筛选通过<Table.Column>组件内,并把filterDropdown回调收到的属性透传给该组件:
<Table.Column dataIndex="status" title="Status" filterDropdown={(props) => ( <FilterDropdown {...props}> <Radio.Group> <Radio value="published">Published</Radio> <Radio value="draft">Draft</Radio> <Radio value="rejected">Rejected</Radio> </Radio.Group> </FilterDropdown> )} />在onChange中,Ant Design 的列级筛选对象(Record<string, FilterValue | null>)通过mapAntdFilterToCrudFilter映射为 Refine 的CrudFilter[],进而触发setFilters。开启syncWithLocation后,筛选状态同样与 URL 双向同步。
初始排序与初始筛选(Initial Filter and Sorter)
使用sorters.initial或filters.initial设置初始状态时,务必同时为<Table.Column>添加getDefaultSortOrder或defaultFilteredValue,否则 Hook 的内部状态可能与表格展示不同步:
const { tableProps, sorters, filters } = useTable({ sorters: { initial: [ { field: "title", order: "asc", }, ], }, filters: { initial: [ { field: "status", operator: "eq", value: "published", }, ], }, }); // --- <Table.Column dataIndex="title" title="Title" defaultSortOrder={getDefaultSortOrder("title", sorters)} /> <Table.Column dataIndex="status" title="Status" render={(value) => <TagField value={value} />} defaultFilteredValue={getDefaultFilter("status", filters)} filterDropdown={(props) => ( <FilterDropdown {...props}> <Radio.Group> <Radio value="published">Published</Radio> <Radio value="draft">Draft</Radio> <Radio value="rejected">Rejected</Radio> </Radio.Group> </FilterDropdown> )} />查找某个字段的筛选值:getDefaultFilter
Refine 提供getDefaultFilter函数(定义于 core 包的definitions/table相关文件中),用于从当前 filters 状态中取出指定字段的筛选值:
import { getDefaultFilter, useTable } from "@refinedev/antd"; const MyComponent = () => { const { filters } = useTable({ filters: { initial: [ { field: "name", operator: "contains", value: "John Doe", }, ], }, }); const nameFilterValue = getDefaultFilter("name", filters, "contains"); console.log(nameFilterValue); // "John Doe" return { /** ... */ }; };getDefaultFilter的第三个参数operator是可选的,传入后可以精确匹配指定运算符的筛选条件。
自定义搜索表单(Search)
useTable提供onSearch与searchFormProps两个属性来构建自定义筛选表单:
onSearch:表单提交时被调用,接收表单值并返回CrudFilters | Promise<CrudFilters>;调用后会把当前页重置为第 1 页;searchFormProps:需要传给 Ant Design<Form>组件的属性。
const { searchFormProps, tableProps } = useTable({ onSearch: (values) => { return [ { field: "title", operator: "contains", value: values.title, }, ]; }, }); <List> <Form {...searchFormProps} layout="inline"> <Form.Item name="title"> <Input placeholder="Search by title" /> </Form.Item> <SaveButton onClick={searchFormProps.form?.submit} /> </Form> <Table {...tableProps} rowKey="id"> <Table.Column title="Title" dataIndex="title" /> </Table> </List>;从 useTable.ts 的onFinish实现可以看到,表单提交时onSearch返回的筛选条件会被setFilters接收,若分页开启则同时setCurrentPage(1)。另外,当开启syncWithLocation后,Hook 会在 effect 中读取表单已注册字段,把 URL 中对应的筛选值回填到表单(见 useTable.ts),实现“URL 即表单状态”的完整闭环。
实时更新(Realtime Updates)
该能力需要配置
LiveProvider才能生效。
当useTable挂载时,它会用channel、resource等参数调用liveProvider的subscribe方法,从而订阅实时更新。配合liveMode: "auto",收到相关事件后数据会自动刷新;使用liveMode: "manual"则需手动处理。更多细节可参考liveProvider文档。
在 antd 适配层中还有一个值得注意的细节:tableProps.loading在liveMode === "auto"时取isLoading,否则取!isFetched(见 useTable.ts),这保证了实时模式下自动更新期间不会闪烁加载态。
全部可配置属性(Properties)详解
下表是useTable的完整配置项。所有属性都透传给 core 层useTable,antd 层仅在此基础上增加onSearch(见 useTable.ts 中的类型定义)。
resource
默认从路由推断resource,也可显式指定:
useTable({ resource: "categories", });如果存在多个同名资源,可以传identifier代替name作为主匹配键;数据提供者的方法仍使用<Refine/>组件中定义的资源name。详见identifier说明。
onSearch
searchFormProps.onFinish被调用时触发,接收表单值,返回CrudFilters | Promise<CrudFilters>,并将当前页重置为 1。适合做任意条件的自定义查询筛选(示例见上文“自定义搜索表单”)。
dataProviderName
存在多个dataProvider时,用它指定当前资源使用哪一个:
useTable({ dataProviderName: "second-data-provider", });pagination.currentPage
设置初始页码,默认1:
useTable({ pagination: { currentPage: 2, }, });pagination.pageSize
设置初始每页条数,默认10:
useTable({ pagination: { pageSize: 20, }, });pagination.mode
取值"off"、"server"或"client",默认"server":
- "off":禁用分页,拉取全部记录;
- "client":客户端分页,先拉取全部记录再在前端切分;
- "server":服务端分页,用
currentPage与pageSize请求对应页数据。
useTable({ pagination: { mode: "client", }, });sorters.initial
设置排序的初始值。initial不是持久的——用户一旦改变排序就会被清除。需要持久排序请用sorters.permanent。排序值类型参见CrudSorting接口:
useTable({ sorters: { initial: [ { field: "name", order: "asc", }, ], }, });sorters.permanent
设置排序的持久值。permanent不可变——用户改变排序时不会被清除。需要临时排序请用sorters.initial:
useTable({ sorters: { permanent: [ { field: "name", order: "asc", }, ], }, });在 core 层实现中,permanent排序会被合并进最终传给useList的sorters(通过unionSorters),而initial只作为useState的初始值,因此用户交互后initial会被覆盖(见 packages/core/src/hooks/useTable/index.ts)。
sorters.mode
取值"off"或"server",默认"server":
- "off":排序值不发送给服务端,可在客户端用
sorters自行排序; - "server":服务端排序,用
sorters值请求数据。
useTable({ sorters: { mode: "server", }, });filters.initial
设置筛选的初始值。initial不是持久的——用户一旦改变筛选就会被清除。需要持久筛选请用filters.permanent。筛选值类型参见CrudFilters接口:
useTable({ filters: { initial: [ { field: "name", operator: "contains", value: "Foo", }, ], }, });filters.permanent
设置筛选的持久值。permanent不可变——用户改变筛选时不会被清除。需要临时筛选请用filters.initial:
useTable({ filters: { permanent: [ { field: "name", operator: "contains", value: "Foo", }, ], }, });filters.defaultBehavior
筛选行为取值"merge"(默认)或"replace":
- "merge":新筛选与现有筛选合并——同一字段的新值替换旧值,不同字段则追加;
- "replace":新筛选整体替换现有筛选——旧筛选全部移除,仅保留新筛选。
可通过setFilters的第二个参数临时覆盖默认行为:
useTable({ filters: { defaultBehavior: "replace", }, });在 core 层 index.ts 中,setFilters被实现为三个分支:merge模式用unionFilters(preferredPermanentFilters, newFilters, prevFilters)合并;replace模式用unionFilters(preferredPermanentFilters, newFilters)替换;函数式调用则基于prevFilters计算。无论哪种模式,permanent筛选都会被unionFilters重新并入,这保证了持久筛选永远存在。
filters.mode
取值"off"或"server",默认"server":
- "off":筛选值不发送给服务端,可在客户端用
filters自行筛选; - "server":服务端筛选,用
filters值请求数据。
useTable({ filters: { mode: "off", }, });syncWithLocation
开启后,useTable的状态(排序、筛选、分页)会自动编码进 URL 查询参数;URL 变化时 Hook 状态也随之更新。这让表格状态可以跨路由/跨页面共享,用户还能为特定表格视图加书签或分享链接。默认false:
useTable({ syncWithLocation: true, });注意:
syncWithLocation也可以在<Refine/>组件 上全局配置。antd 适配层会通过useSyncWithLocation()读取全局默认值,并以 props 传入值为准(见 useTable.ts)。
queryOptions
useTable通过useList获取数据,因此可以把queryOptions透传给它(如retry、staleTime等):
useTable({ queryOptions: { retry: 3, }, });meta
meta用于向数据提供者方法传递附加信息,常见用途:
- 针对特定用例定制数据提供者方法;
- 用纯 JavaScript 对象(JSON)生成 GraphQL 查询。
useTable({ meta: { headers: { "x-meta-data": "true" }, }, }); const myDataProvider = { //... getList: async ({ resource, pagination, sorters, filters, meta, }) => { const headers = meta?.headers ?? {}; const url = `${apiUrl}/${resource}`; //... const { data, headers } = await httpClient.get(`${url}`, { headers }); return { data, }; }, //... };meta 的通用概念详见 General Concepts 文档。
successNotification
需要配置
NotificationProvider才能生效。
数据获取成功后,useTable会调用NotificationProvider的open方法弹出成功通知,可用该属性自定义:
useTable({ successNotification: (data, values, resource) => { return { message: `${data.title} Successfully fetched.`, description: "Success with no errors", type: "success", }; }, });errorNotification
需要配置
NotificationProvider才能生效。
数据获取失败时弹出错误通知,可用该属性自定义:
useTable({ errorNotification: (data, values, resource) => { return { message: `Something went wrong when getting ${data.id}`, description: "Error", type: "error", }; }, });liveMode
需要配置
LiveProvider才能生效。
决定收到相关实时事件后是否自动更新数据:"auto"自动更新,"manual"手动处理。详见 Live / Realtime 文档:
useTable({ liveMode: "auto", });onLiveEvent
需要配置
LiveProvider才能生效。
订阅到新事件时执行的回调函数:
useTable({ onLiveEvent: (event) => { console.log(event); }, });liveParams
需要配置
LiveProvider才能生效。
传递给liveProvider.subscribe方法的参数,详见 subscribe。
overtimeOptions
为请求启用加载超时检测,适合在请求耗时过长时展示加载提示:
interval:时间间隔(毫秒);onInterval:每个间隔触发一次的函数。
Hook 返回的overtime.elapsedTime表示已耗时(毫秒),请求完成后变为undefined:
const { overtime } = useTable({ //... overtimeOptions: { interval: 1000, onInterval(elapsedInterval) { console.log(elapsedInterval); }, }, }); console.log(overtime.elapsedTime); // undefined, 1000, 2000, 3000 4000, ... // You can use it like this: { elapsedTime >= 4000 && <div>this takes a bit longer than expected</div>; }返回值(Return Values)详解
tableProps
传给 Ant Design<Table>组件的属性集合,包含:
onChange:用户对表格进行操作(筛选、排序等)时执行的回调。
⚠️ 注意:
useTable正是通过该函数处理排序、筛选与分页的。如果你覆写onChange,就必须手动处理这些操作。const { tableProps } = useTable(); <Table {...tableProps} onChange={tableProps.onChange} rowKey="id"> <Table.Column title="Title" dataIndex="title" /> </Table>;dataSource:表格展示的数据,即
useList获取到的记录。loading:数据是否正在获取。
pagination:分页配置值(
pageSize、currentPage、position等)。scroll:表格是否可滚动,默认
{ x: true }。
searchFormProps
返回 Ant Design<Form>实例相关属性。当searchFormProps.onFinish被调用时会触发onSearch;也可用searchFormProps.form.submit手动提交表单。典型用法是构建表格上方的筛选表单:
import { HttpError } from "@refinedev/core"; import { List, useTable, SaveButton } from "@refinedev/antd"; import { Table, Form, Input } from "antd"; interface IPost { id: number; title: string; } interface ISearch { title: string; } const PostList: React.FC = () => { const { searchFormProps, tableProps } = useTable<IPost, HttpError, ISearch>({ onSearch: (values) => { return [ { field: "title", operator: "contains", value: values.title, }, ]; }, }); return ( <List> <Form {...searchFormProps} layout="inline"> <Form.Item name="title"> <Input placeholder="Search by title" /> </Form.Item> <SaveButton onClick={searchFormProps.form?.submit} /> </Form> <Table {...tableProps} rowKey="id"> <Table.Column dataIndex="id" title="ID" /> <Table.Column title="Title" dataIndex="title" /> </Table> </List> ); };tableQuery
即useList的返回值(react-query的useQuery结果)。
sorters / setSorters
sorters:当前排序状态;setSorters:设置排序状态的函数,类型为(sorters: CrudSorting) => void。
filters / setFilters
filters:当前筛选状态;setFilters:设置筛选状态的函数,支持两种调用方式:
((filters: CrudFilters, behavior?: SetFilterBehavior) => void) & ((setter: (prevFilters: CrudFilters) => CrudFilters) => void);currentPage / setCurrentPage / pageSize / setPageSize
currentPage:当前页码状态;分页禁用时为undefined;setCurrentPage:React.Dispatch<React.SetStateAction<number>> | undefined;pageSize:当前每页条数;分页禁用时为undefined;setPageSize:React.Dispatch<React.SetStateAction<number>> | undefined。
pageCount
总页数状态;分页禁用时为undefined。在 core 层通过Math.ceil(total / pageSize)计算(见 index.ts)。
createLinkForSyncWithLocation
生成syncWithLocation可用链接的函数,接收SyncWithLocationParams并返回 URL 字符串:
(params: SyncWithLocationParams) => string;overtime
返回{ elapsedTime?: number },elapsedTime为已耗时(毫秒),请求完成后为undefined:
const { overtime } = useTable(); console.log(overtime.elapsedTime); // undefined, 1000, 2000, 3000 4000, ...常见问题(FAQ)
如何处理关系数据?
使用useMany获取关系数据,并借助useSelect实现按分类(categories)筛选<Table>。典型的“博客文章 + 分类”列表页会先用useTable拿到文章,再从文章数据中提取分类 ID,通过useMany批量查询分类名称,最后用useSelect生成分类筛选下拉框。
如何做客户端筛选?
将filters.mode设为"off"以禁用服务端筛选,此时useTable与 Ant Design<Table>自带的列筛选功能完全兼容:
import { useTable } from "@refinedev/antd"; import { Table } from "antd"; const ListPage = () => { const { tableProps } = useTable({ filters: { mode: "off", }, }); return ( <Table {...tableProps} rowKey="id"> {/* ... */} <Table.Column dataIndex="status" title="Status" filters={[ { text: "Published", value: "published", }, { text: "Draft", value: "draft", }, { text: "Rejected", value: "rejected", }, ]} onFilter={(value, record) => record.status === value} /> </Table> ); };如何做客户端排序?
将sorters.mode设为"off"以禁用服务端排序,此时可完全使用 Ant Design<Table>自带的列排序功能:
import { useTable } from "@refinedev/antd"; import { Table } from "antd"; const ListPage = () => { const { tableProps } = useTable({ sorters: { mode: "off", }, }); return ( <Table {...tableProps} rowKey="id"> <Table.Column dataIndex="id" title="ID" sorter={(a, b) => a.id - b.id} /> {/* ... */} </Table> ); };API 速查
类型参数(Type Parameters)
| 属性 | 说明 | 类型 | 默认值 |
|---|---|---|---|
| TQueryFnData | 查询函数返回的结果数据,继承BaseRecord | BaseRecord | BaseRecord |
| TError | 自定义错误对象,继承HttpError | HttpError | HttpError |
| TSearchVariables | 搜索参数的值类型 | {} | |
| TData | select函数返回的结果数据,继承BaseRecord;未指定时默认使用TQueryFnData | BaseRecord | TQueryFnData |
返回值
| 属性 | 说明 | 类型 |
|---|---|---|
| searchFormProps | Ant Design<Form>的属性 | FormProps<TSearchVariables> |
| tableProps | Ant Design<Table>的属性 | TableProps<TData> |
| tableQuery | react-query的useQuery结果 | QueryObserverResult<{ data: TData[]; total: number; }, TError> |
| totalPage | 总页数(分页禁用时为undefined) | number \| undefined |
| currentPage | 当前页码状态(分页禁用时为undefined) | number \| undefined |
| setCurrentPage | 修改当前页的函数(分页禁用时为undefined) | React.Dispatch<React.SetStateAction<number>> \| undefined |
| pageSize | 当前每页条数(分页禁用时为undefined) | number \| undefined |
| setPageSize | 修改每页条数的函数(分页禁用时为undefined) | React.Dispatch<React.SetStateAction<number>> \| undefined |
| sorters | 当前排序状态 | CrudSorting |
| setSorters | 接受新排序状态的函数 | (sorters: CrudSorting) => void |
| filters | 当前筛选状态 | CrudFilters |
| setFilters | 接受新筛选状态的函数 | -(filters: CrudFilters, behavior?: "merge" \| "replace" = "merge") => void-(setter: (previousFilters: CrudFilters) => CrudFilters) => void |
| overtime | 超时加载属性 | { elapsedTime?: number } |
旧版 API 中的
sorter/setSorter已废弃,请使用sorters/setSorters。
完整示例
官方提供可直接运行的沙箱示例:
- table-antd-use-table:本文所述基础用法(分页、排序、筛选)的完整项目,包含
App.tsx、数据提供者配置与页面实现,可对照本文逐步阅读。 - 端到端测试位于 cypress/e2e/table-antd-use-table,覆盖了列表加载、排序、筛选等交互断言,可作为验收标准参考。
总结
useTable是连接 Refine 数据层与 Ant Design 表格组件的桥梁:core 层负责分页、排序、筛选的状态管理与数据请求(基于useList),antd 层负责将其翻译成<Table>可消费的 props,并额外提供searchFormProps搜索表单能力。理解syncWithLocation、initial/permanent、mode、defaultBehavior这几组核心概念,就能在不同业务场景(服务端/客户端分页与筛选、可分享的表格视图、实时刷新的看板)中游刃有余。官方文档 index.md、antd 实现 useTable.ts 与 core 实现 index.ts 是继续深入的最佳起点。
【免费下载链接】refineA React Framework for building internal tools, admin panels, dashboards & B2B apps with unmatched flexibility.项目地址: https://gitcode.com/GitHub_Trending/re/refine
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考