refine v3 中 useTable 搜索表单实战:基于 onSearch 与 searchFormProps 实现 Ant Design 表格自定义搜索
【免费下载链接】refineA React Framework for building internal tools, admin panels, dashboards & B2B apps with unmatched flexibility.项目地址: https://gitcode.com/GitHub_Trending/re/refine
导读
本篇技术指南围绕 refine 项目(v3 分支)Ant Design 集成中useTable的Search(自定义搜索表单)能力展开,以官方文档中的实时预览代码块 _partial-use-table-search-live-preview.md 为骨架,结合源码与可运行示例深入讲解onSearch与searchFormProps的完整用法。读完本文,你将掌握:如何在 refine + Ant Design 项目中搭建一个与表格联动、支持任意查询条件的搜索表单,理解搜索提交后过滤条件如何转化为CrudFilters并驱动数据请求,以及如何与分页、syncWithLocation、初始/永久过滤等机制协同工作。
一、Search 功能概述:为什么需要自定义搜索表单
在 refine 的useTable中,基础用法、排序(Sorting)、筛选(Filtering)等能力开箱即用,但当你需要一个独立于表格列头的、自由组合的搜索区域时(例如在表格上方放置一个"按标题搜索"的输入框),就需要使用 Search 特性。
正如 useTable.md 文档所述:
We can use
onSearchandsearchFormPropsproperties to make custom filter form.onSearchis a function that is called when the form is submitted.searchFormPropsis a property that is passed to the<Form>component.
即:
onSearch:表单提交时被调用的函数,接收表单的值,返回CrudFilters(或返回该类型的 Promise);searchFormProps:需要透传给 Ant Design<Form>组件的属性,让表单与useTable的搜索逻辑绑定。
useTable本身是 Ant Design 集成层对核心包@pankod/refine-core中useTable的扩展,底层通过useList拉取数据,因此搜索、排序、分页、筛选的过滤条件最终都会统一收敛到数据请求中。
二、完整示例:在文章列表上方实现"按标题搜索"
官方实时预览代码块演示了最典型的使用场景——在posts列表页顶部放置一个内联搜索表单,输入标题后提交,表格即按标题进行模糊匹配过滤。完整代码如下(取自关联文档,可直接运行):
import { IResourceComponentsProps, HttpError } from "@pankod/refine-core"; import { List, Table, TagField, useTable, // highlight-start Form, SaveButton, Input, // highlight-end } from "@pankod/refine-antd"; interface IPost { id: number; title: string; content: string; status: "published" | "draft" | "rejected"; } interface ISearch { title: string; } const PostList: React.FC<IResourceComponentsProps> = () => { const { tableProps, searchFormProps } = useTable<IPost, HttpError, ISearch>( { // highlight-start onSearch: (values) => { return [ { field: "title", operator: "contains", value: values.title, }, ]; }, // highlight-end }, ); return ( <List> {/* highlight-start */} <Form {...searchFormProps} layout="inline"> <Form.Item name="title"> <Input placeholder="Search by title" /> </Form.Item> <SaveButton onClick={searchFormProps.form?.submit} /> </Form> {/* highlight-end */} <Table {...tableProps} rowKey="id"> <Table.Column dataIndex="id" title="ID" /> <Table.Column dataIndex="title" title="Title" /> <Table.Column dataIndex="content" title="Content" /> <Table.Column dataIndex="status" title="Status" render={(value: string) => <TagField value={value} />} /> </Table> </List> ); };2.1 逐段拆解
- 泛型三参数:
useTable<IPost, HttpError, ISearch>中第三个泛型ISearch用于声明搜索表单的字段结构(此处为{ title: string }),这也是useTable的三个类型参数之一(TSearchVariables,默认值为{})。 onSearch返回过滤条件:提交时拿到values,返回一个包含单个CrudFilter的数组{ field: "title", operator: "contains", value: values.title },语义是"标题字段包含输入内容"。这里返回的CrudFilters会替换/合并进当前过滤状态,并随下一次数据请求发送给数据提供者。- 表单绑定:
<Form {...searchFormProps} layout="inline">将useTable内部的表单实例与 Ant Design<Form>打通;<Form.Item name="title">的name必须与ISearch字段、onSearch读取的键保持一致。 - 手动触发提交:
<SaveButton onClick={searchFormProps.form?.submit} />通过表单实例的submit()主动提交,从而触发onSearch。SaveButton是@pankod/refine-antd提供的按钮组件,也可以替换为普通<Button htmlType="submit">。
2.2 关于实时预览代码块的说明
关联文档本身是 Docusaurus 文档中的 live-preview 片段(url=http://localhost:3000/posts、previewHeight=420px),除上述主体代码外还包含预览环境相关代码:
setInitialRoutes(["/posts"]); // visible-block-start // ...上面展示的 PostList 组件代码... // visible-block-end setRefineProps({ resources: [ { name: "posts", list: PostList, }, ], }); render(<RefineAntdDemo />);其中setInitialRoutes(["/posts"])将初始路由指向/posts(与预览地址localhost:3000/posts一致),setRefineProps注册posts资源并把PostList作为其list页面,render(<RefineAntdDemo />)挂载演示应用。在实际项目代码中,你只需要关注visible-block-start与visible-block-end之间的PostList组件即可。
三、深入源码:搜索提交背后的调用链
要真正理解 Search 特性,需要看useTable的 Ant Design 集成实现。核心文件是 packages/antd/src/hooks/table/useTable/useTable.ts。
3.1 类型定义
该 Hook 扩展了核心useTable的入参与返回值:
export type useTableProps<TQueryFnData, TError, TSearchVariables, TData> = useTablePropsCore<TQueryFnData, TError, TData> & { onSearch?: (data: TSearchVariables) => CrudFilters | Promise<CrudFilters>; }; export type useTableReturnType<TData, TError, TSearchVariables> = useTableCoreReturnType<TData, TError> & { searchFormProps: FormProps<TSearchVariables>; tableProps: TableProps<TData>; };可以看到onSearch的签名正是"接收表单值 → 返回CrudFilters | Promise<CrudFilters>",支持异步处理(例如先请求某个接口再构造过滤条件)。
3.2 onFinish 的实现
searchFormProps的关键在于它被注入了自定义的onFinish(useTable.ts):
const onFinish = async (value: TSearchVariables) => { if (onSearch) { const searchFilters = await onSearch(value); setFilters(searchFilters); if (isPaginationEnabled) { setCurrentPage?.(1); } } };从源码可以确认两个重要的行为事实:
- 提交即过滤:表单提交后,
onSearch(value)的返回值被直接传入setFilters(searchFilters),从而更新useTable的过滤状态并触发重新请求。文档中"onSearchwill set the current page to 1"(将当前页重置为 1)在源码中也有明确对应:setCurrentPage?.(1),这保证了搜索后总是从第一页开始展示结果。 - 返回值被透传:
searchFormProps是由内部表单实例的formSF.formProps展开并覆盖onFinish后得到的:
return { searchFormProps: { ...formSF.formProps, onFinish, }, // ... };此外,内部通过Form.useForm<TSearchVariables>()创建表单实例,因此你可以通过searchFormProps.form?.submit()以命令式方式提交表单,这正是示例中SaveButton的用法。
3.3 setFilters 与过滤行为
setFilters来自核心包 packages/core/src/hooks/useTable/index.ts,其签名支持两种调用方式:
setFilters: ((filters: CrudFilter[], behavior?: SetFilterBehavior) => void) & ((setter: (previousFilters: CrudFilter[]) => CrudFilter[]) => void);其中SetFilterBehavior = "merge" | "replace",默认行为是merge(可在useTable的defaultSetFilterBehavior属性中改为"replace",也可在调用setFilters时通过第二参数临时覆盖):
merge(默认):新过滤条件与现有过滤条件合并——若新条件与已有条件同字段,则以新条件替换该字段的旧条件;若字段不同,则追加到现有条件中。replace:用新过滤条件整体替换所有现有过滤条件。
对搜索场景而言,默认的merge意味着:搜索产生的标题过滤会与列头筛选、permanentFilter等并存,互不覆盖;而如果你希望搜索条件"一锤定音"地覆盖其他过滤,则可显式指定replace行为。
四、CrudFilters 与操作符:搜索条件的"标准语"
onSearch的返回值类型CrudFilters本质是CrudFilter[],每个CrudFilter由field、operator、value三部分组成,定义见 interfaces.md:
| Key | Type | 说明 |
|---|---|---|
| field | string | 过滤的字段名 |
| operator | CrudOperators | 过滤操作符 |
| value | any | 过滤值(可为空) |
CrudOperators覆盖了常见的比较与字符串匹配语义,包括:
"eq" | "ne" | "lt" | "gt" | "lte" | "gte" | "in" | "nin" | "contains" | "ncontains" | "containss" | "ncontainss" | "between" | "nbetween" | "null" | "nnull" | "startswith" | "nstartswith" | "startswiths" | "nstartswiths" | "endswith" | "nendswith" | "endswiths" | "nendswiths";常用操作符速查:
| 操作符 | 含义 |
|---|---|
"eq" | 等于 |
"ne" | 不等于 |
"in" | 在数组中 |
"nin" | 不在数组中 |
"contains" | 包含(不区分大小写) |
"containss" | 包含(区分大小写) |
"between" | 介于两个值之间 |
"null" | 为空 |
"nnull" | 不为空 |
"startswith" | 以指定字符串开头 |
"endswith" | 以指定字符串结尾 |
示例中的operator: "contains"即"标题包含输入值"的模糊搜索。如果希望搜索区分大小写,可改用"containss"。这些操作符最终会由数据提供者(如 REST 提供者)转换为对应的查询参数,因此请以你所使用的数据提供者对操作符的实际支持为准。
五、更完整的搜索与过滤组合:借鉴官方示例项目
官方示例 examples/table-antd-use-table 提供了比 live-preview 更完整的列表页实践,可与 Search 特性互补理解。它展示了:
syncWithLocation: true:将分页、排序、过滤状态同步到 URL 查询参数,便于分享与书签;filters.initial初始过滤:例如{ field: "title", operator: "contains", value: "" }、{ field: "status", operator: "eq", value: "draft" }、{ field: "category.id", operator: "in", value: [1, 2] };sorters.initial初始排序:如按title升序;- 列头过滤:通过
FilterDropdown组件包裹Input、Radio.Group、多选Select实现按列过滤,并用getDefaultFilter/getDefaultSortOrder(定义于 packages/antd/src/definitions/table/index.ts)将当前过滤、排序状态回填到列头。
需要说明的是:Search(表单搜索)与 Filtering(列头筛选)是两条独立但共享同一份filters状态的路径。列头筛选经由<Table>的onChange回调中的mapAntdFilterToCrudFilter(同样位于 packages/antd/src/definitions/table/index.ts)转换为CrudFilters;而表单搜索则经由onSearch→setFilters进入同一状态。二者都遵循merge/replace行为规则。
此外,示例中还使用了initialFilter/permanentFilter、initialSorter/permanentSorter的概念——initial*是"临时的",用户一旦手动变更即被清除;permanent*是"永久的",不可被用户操作清除。在配置搜索相关过滤时,可根据业务需要选择合适的一类。
六、Search 与 syncWithLocation 的协同
当启用syncWithLocation时,搜索表单与 URL 之间存在双向同步。在 useTable.ts 中有如下逻辑:
React.useEffect(() => { if (shouldSyncWithLocation) { // get registered fields of form const registeredFields = formSF.form.getFieldsValue() as Record<string, any>; // map `filters` for registered fields const filterFilterMap = Object.keys(registeredFields).reduce( (acc, curr) => { const filter = filters.find( (filter) => "field" in filter && filter.field === curr, ); if (filter) { acc[curr] = filter?.value; } return acc; }, {} as Record<string, any>, ); // set values to form formSF.form.setFieldsValue(filterFilterMap as any); } }, [shouldSyncWithLocation]);也就是说:当 URL 查询参数中包含与表单字段同名的过滤条件时,useTable会把这些值回填到搜索表单中。这样用户通过 URL 打开页面时,搜索框会自动带上上一次的搜索内容,实现可分享、可书签的"搜索视图"。
对应的测试用例位于 packages/antd/src/hooks/table/useTable/useTable.spec.tsx,其中 "should pass form values to search form from params (syncWithLocation)" 用例验证了"从 URL 参数回填表单值"这一行为,是 Search +syncWithLocation协同工作的直接测试证据。
七、实战要点与注意事项
Form.Item的name必须与onSearch读取的字段一致:例如name="title"对应values.title,否则拿到的值将是undefined。- 提交方式:既可以使用
<SaveButton onClick={searchFormProps.form?.submit} />命令式提交,也可以在<Form>内放置<Button htmlType="submit">利用表单原生提交;两种方式都会触发onSearch。 - 搜索后分页重置:
onSearch触发后当前页会自动重置为 1(源码中setCurrentPage?.(1)),因此无需手动处理"搜索后停留在第 5 页"的问题。 - 异步搜索:
onSearch支持返回Promise<CrudFilters>,可在其中先调用接口(如根据关键词预查询关联 ID)再构造过滤条件。 - 与列头筛选的关系:搜索表单与列头筛选共享过滤状态,默认
merge行为下它们会叠加生效;若需互斥,可把defaultSetFilterBehavior设为"replace"。 - 类型安全:善用第三个泛型参数
TSearchVariables约束表单数据结构,searchFormProps会以FormProps<TSearchVariables>形式提供类型提示。 - 底层数据获取:
useTable底层使用useList拉取数据(参见 useList 文档),搜索产生的过滤条件会随getList请求一并发送给数据提供者,因此搜索能力本身不依赖任何 UI 框架,Ant Design 集成只是提供了便捷的表单封装。
八、关联资源索引
- 本文骨架来源:_partial-use-table-search-live-preview.md(Search 实时预览代码块)
- 父级 API 文档:useTable.md(含
onSearch、searchFormProps完整属性说明) - 实现源码:packages/antd/src/hooks/table/useTable/useTable.ts(
onFinish、searchFormProps、onChange实现) - 核心 Hook:packages/core/src/hooks/useTable/index.ts(
setFilters、过滤行为、syncWithLocation) - 类型与操作符:interfaces.md(
CrudFilters、CrudOperators) - 过滤转换工具:packages/antd/src/definitions/table/index.ts(
mapAntdFilterToCrudFilter、getDefaultFilter、getDefaultSortOrder) - 可运行示例:examples/table-antd-use-table/src/pages/posts/list.tsx(
syncWithLocation、初始/永久过滤、列头筛选综合实践) - 相关测试:packages/antd/src/hooks/table/useTable/useTable.spec.tsx(
syncWithLocation表单回填行为验证)
【免费下载链接】refineA React Framework for building internal tools, admin panels, dashboards & B2B apps with unmatched flexibility.项目地址: https://gitcode.com/GitHub_Trending/re/refine
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考