Refine useSelect Hook 实战指南:将 Ant Design Select 与资源数据无缝绑定
【免费下载链接】refineA React Framework for building internal tools, admin panels, dashboards & B2B apps with unmatched flexibility.项目地址: https://gitcode.com/GitHub_Trending/re/refine
useSelect是 Refine 面向 Ant Design 提供的字段级 Hook,用于把一个资源(resource)中的记录直接转换成 Ant Design<Select>组件的选项(options)。它内部基于useList完成数据拉取,并自动处理加载态、搜索(含防抖)、排序、过滤与分页。读完本文,你将掌握useSelect的完整属性体系、返回值语义、与useForm的组合方式,以及它在 @refinedev/antd 源码 与 @refinedev/core 源码 中的真实实现原理。
本文以 documentation/docs/ui-integrations/ant-design/hooks/use-select/index.md 及其配套的可运行示例(_basic-usage-live-preview.md、_on-search-live-preview.md、_sort-live-preview.md、_default-value-live-preview.md、_crud-live-preview.md)为主体展开。
useSelect 是什么:定位与数据流
useSelect允许你在资源记录需要作为下拉选项时,管理 Ant Design 的<Select>组件。它把"取数据"这件事完全交给 Refine:
- 主查询走
useList(调用dataProvider.getList),用于拉取选项列表; - 当配置了
defaultValue时,会额外走useMany(调用dataProvider.getMany),用于补齐"默认选中项"的数据。
用一句话概括:你只需要声明资源名,useSelect负责把数据变成{ label, value }形状的 options,并把这些 options 连同搜索、加载态一起打包成selectProps,直接展开到<Select>上即可。
关于
useList的更多细节,可参考 useList 文档。
快速上手:基础用法
先看一个完整的入门示例——在"创建文章"页面中,从categories资源加载分类下拉选项:
import { useSelect } from "@refinedev/antd"; import { Select } from "antd"; interface ICategory { id: number; title: string; } const PostCreate: React.FC = () => { const { selectProps } = useSelect<ICategory>({ resource: "categories", }); return ( <Select placeholder="Select a category" style={{ width: 300 }} {...selectProps} /> ); };把selectProps展开到<Select>上,即获得了:
options:由categories资源记录转换而成的{ label, value }[];onSearch:内置的搜索回调(默认对title字段做contains过滤);loading:数据拉取中的加载状态;showSearch: true与filterOption: false:开启搜索框,但把过滤逻辑交给服务端(由onSearch触发的getList请求完成)。
这些默认行为在 packages/antd/src/hooks/fields/useSelect/index.ts 中可见一斑。
重要:useSelect 不管理选中值
useSelect主要面向数据获取(管理 options、loading、分页),不管理<Select>的受控状态(当前选中值)。如果独立使用<Select>,你需要自己用useState维护value/onChange;如果与 Ant Design Form 一起使用,则把<Select>放进Form.Item,由表单负责选中值。
核心属性配置详解
resource(必填)
resource会经由useList作为参数传给dataProvider的getList方法。它通常被当作 API 端点路径使用,但具体如何解释取决于你的getList实现(参见 创建 data provider 文档)。
useSelect({ resource: "categories", });如果存在多个同名资源,可以传入identifier来替代name作为资源的匹配键;data provider 方法仍使用<Refine />组件中定义的name工作。相关说明见<Refine />组件的identifier章节。
optionLabel 与 optionValue
用于自定义选项的value与label。默认值分别为optionLabel = "title"、optionValue = "id":
useSelect<ICategory>({ resource: "products", optionLabel: "name", optionValue: "productId", });这两个属性支持Object path 嵌套访问(lodash 风格,即get(item, path)):
const { options } = useSelect({ resource: "categories", optionLabel: "nested.title", optionValue: "nested.id", });也支持传入函数,函数会收到item参数,便于拼接显示:
const { options } = useSelect({ optionLabel: (item) => `${item.firstName} ${item.lastName}`, optionValue: (item) => item.id, });从源码看,core 层通过getOptionLabel/getOptionValue两个useCallback统一处理"字符串路径"与"函数"两种形态(见 packages/core/src/hooks/useSelect/index.ts)。
searchField
指定onSearch时按哪个字段进行搜索:
const { onSearch } = useSelect({ searchField: "name" }); onSearch("John"); // 按 name 字段、值为 John 搜索默认规则:当optionLabel是字符串时,沿用optionLabel的值;否则回退到title字段:
// optionLabel 为字符串时,搜索 name 字段 const { onSearch } = useSelect({ optionLabel: "name" }); onSearch("John"); // 按 name 搜索 // optionLabel 为函数时,回退到 title 字段 const { onSearch } = useSelect({ optionLabel: (item) => `${item.id} - ${item.name}`, }); onSearch("John"); // 按 title 搜索sorters
控制选项的展示顺序,会经useList传给getList,最终以排序查询参数的形式发给 API:
useSelect({ sorters: [ { field: "title", order: "asc", }, ], });你甚至可以把排序字段与顺序做成受控状态,实现"一键切换排序":
const [order, setOrder] = React.useState<"asc" | "desc">("asc"); const { selectProps } = useSelect<ICategory>({ resource: "categories", sorters: [ { field: "title", order, }, ], }); return ( <> <Select placeholder={`Ordered Categories: ${order}`} style={{ width: 300 }} {...selectProps} /> <Button onClick={() => setOrder(order === "asc" ? "desc" : "asc")}> Toggle Order </Button> </> );sorter的完整形态可参考 CrudSorting 接口文档。
filters
过滤展示的选项,同样会经useList传给getList:
useSelect({ filters: [ { field: "isActive", operator: "eq", value: true, }, ], });完整的过滤器形态见 CrudFilters 接口文档。
defaultValue:保证默认值出现在选项中
当选项数量很多、需要分页时,某个"默认选中值"可能不在当前可见列表中,导致<Select>显示异常。为此,useSelect会在配置defaultValue时额外发起一次useMany查询,把默认值对应的记录补充进 options。defaultValue可以是单个值,也可以是数组:
useSelect({ defaultValue: 1, // 或 [1, 2] });:::info 注意defaultValue并不设置默认选中项,它只保证该值存在于选项中。要真正默认选中,请把值传给<Select>的valueprop 或useForm:
const form = useForm({ defaultValues: { category: { id: 1 }, // 默认选中值 }, }); const { selectProps } = useSelect({ resource: "categories", defaultValue: [1], // 确保默认值出现在选项中 });:::
selectedOptionsOrder
控制defaultValue对应的"已选选项"在列表中的位置:
"in-place":已选选项排在底部(默认值);"selected-first":已选选项排在顶部。
useSelect({ defaultValue: 1, // 或 [1, 2] selectedOptionsOrder: "selected-first", // in-place | selected-first });这背后依赖
useMany查询,可参考 useMany 文档。
debounce
对onSearch函数做防抖处理,单位为毫秒(core 层默认值为300):
useSelect({ resource: "categories", debounce: 500, });queryOptions
透传给内部useQuery的额外选项,例如控制重试次数:
useSelect({ queryOptions: { retry: 3, }, });pagination
分页配置会经useList传给getList,用于发送分页查询参数。它支持以下子项:
currentPage:指定页码
useSelect({ pagination: { currentPage: 2, }, });pageSize:每页条数
useSelect({ pagination: { pageSize: 20, }, });mode:"off"、"client"或"server",决定是否使用服务端分页
useSelect({ pagination: { mode: "off", }, });值得注意的是,当pageSize未指定时,core 层默认使用10(见 packages/core/src/hooks/useSelect/index.ts)。
defaultValueQueryOptions
当传入defaultValue时,会调用useMany查询已选记录。defaultValueQueryOptions用来定制这次查询的选项;如果未传入defaultValue,则会回退使用queryOptions中的值:
const { options } = useSelect({ resource: "categories", defaultValueQueryOptions: { onSuccess: (data) => { console.log("triggers when on query return on success"); }, }, });onSearch:给 options 加上 Autocomplete
onSearch允许为选项加入 Autocomplete(自动补全)能力。它的签名是(value: string) => CrudFilter[],即"输入值 → 过滤条件":
import { useSelect } from "@refinedev/antd"; import { Select } from "antd"; interface ICategory { id: number; title: string; } const PostCreate: React.FC = () => { const { selectProps } = useSelect<ICategory>({ resource: "categories", onSearch: (value) => [ { field: "title", operator: "contains", value, }, ], }); return ( <Select placeholder="Select a category" style={{ width: 300 }} {...selectProps} /> ); };注意:一旦使用onSearch,它会覆盖已有的filters(搜索条件会整体替换原过滤条件)。原因是 core 层在发起useList时执行filters.concat(search),而onSearch返回的数组会整体写入search状态(见 packages/core/src/hooks/useSelect/index.ts)。
客户端过滤:不请求服务端
如果你希望完全在客户端过滤选项,把onSearch显式传为undefined,并设置filterOption与optionFilterProp:
const { selectProps } = useSelect({ resource: "categories", }); <Select {...selectProps} onSearch={undefined} filterOption={true} optionFilterProp="label" // 或 "value" />;meta
meta用于向 data provider 方法传递额外信息,典型用途包括:
- 针对特定场景定制 data provider 方法;
- 用纯 JavaScript 对象(JSON)生成 GraphQL 查询。
例如,向getList传递自定义请求头:
useSelect({ 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, }; }, //... };更系统的说明见 General Concepts 的 meta 概念章节。
dataProviderName
当项目配置了多个 data provider 时,用它指定使用哪一个:
useSelect({ dataProviderName: "second-data-provider", });successNotification 与 errorNotification
在数据成功/失败拉取后,useSelect可以调用NotificationProvider的open方法展示通知。这两个 prop 用于自定义通知内容(依赖 NotificationProvider):
useSelect({ successNotification: (data, values, resource) => { return { message: `${data.title} Successfully fetched.`, description: "Success with no errors", type: "success", }; }, });useSelect({ errorNotification: (data, values, resource) => { return { message: `Something went wrong when getting ${data.id}`, description: "Error", type: "error", }; }, });实时更新相关:liveMode / onLiveEvent / liveParams
以下属性依赖 LiveProvider:
liveMode:收到相关 live 事件后,是否自动("auto")或手动("manual")更新数据;
useSelect({ liveMode: "auto", });onLiveEvent:订阅到新事件时执行的回调;
useSelect({ onLiveEvent: (event) => { console.log(event); }, });liveParams:传给liveProvider的subscribe方法的参数。
当useSelect挂载时,它会向subscribe方法传递channel、resource等参数,从而实现实时订阅。
overtimeOptions:超时加载提示
当你希望在请求耗时过长时展示加载提示,可以传入overtimeOptions。interval是毫秒级的时间间隔,onInterval是每个间隔触发的回调:
const { overtime } = useSelect({ //... overtimeOptions: { interval: 1000, onInterval(elapsedInterval) { console.log(elapsedInterval); }, }, }); console.log(overtime.elapsedTime); // undefined, 1000, 2000, 3000 4000, ... // 用法示例: { elapsedTime >= 4000 && <div>this takes a bit longer than expected</div>; }overtime.elapsedTime表示已耗时毫秒数,请求完成后会变为undefined。
返回值速查
| 返回值 | 说明 |
|---|---|
selectProps | 可直接展开到<Select>的 Ant Design 属性(options、onSearch、loading、showSearch、filterOption) |
query | 列表查询结果(useList对应的QueryObserverResult) |
defaultValueQuery | defaultValue记录的查询结果(useMany对应结果) |
defaultValueQueryOnSuccess | 默认值查询成功时的回调 |
overtime | 超时加载属性,含elapsedTime |
实战问答:高频场景速查
如何给 options 加搜索(Autocomplete)?
使用onSearch,它负责设置搜索值并触发服务端过滤。完整示例见上文 onSearch 一节,以及配套的_on-search-live-preview.md示例。
如何确保defaultValue出现在选项中?
有时我们只拿到id,却希望它在选择框中被显示为已选。useSelect会通过useMany拉取该记录数据并标记为已选(示例见 _default-value-live-preview.md):
const { selectProps } = useSelect<ICategory>({ resource: "categories", defaultValue: 11, });如何修改 options 的 label 和 value?
使用optionLabel与optionValue,默认是title与id。要改成name和categoryId:
useSelect({ optionLabel: "name", optionValue: "categoryId", });可以手动创建 options 吗?
有时仅靠optionLabel/optionValue不够灵活,可以直接从query返回值手动构造 options:
const { query } = useSelect(); const options = query.data?.data.map((item) => ({ label: item.title, value: item.id, })); return <Select options={options} />;如何与 CRUD 组件和 useForm 配合?
把selectProps放进Form.Item,与useForm的formProps、saveButtonProps组合即可(示例见 _crud-live-preview.md):
import { Create, useSelect, useForm } from "@refinedev/antd"; import { Form, Select } from "antd"; interface ICategory { id: number; title: string; } const PostCreate: React.FC = () => { const { formProps, saveButtonProps } = useForm<ICategory>(); const { selectProps } = useSelect<ICategory>({ resource: "categories", }); return ( <Create saveButtonProps={saveButtonProps}> <Form {...formProps} layout="vertical"> <Form.Item label="Category" placeholder="Select a category" name={["category", "id"]} rules={[ { required: true, }, ]} > <Select {...selectProps} /> </Form.Item> </Form> </Create> ); };源码级原理剖析
第一层:@refinedev/antd 的轻量封装
packages/antd/src/hooks/fields/useSelect/index.ts 中的useSelect只是一个"适配层":它调用 core 层的useSelectCore拿到query、defaultValueQuery、onSearch、options,再组装成 Ant Design 的selectProps:
return { selectProps: { options, onSearch, loading: defaultValueQuery.query.isFetching, showSearch: true, filterOption: false, }, query, defaultValueQuery: defaultValueQuery.query, };可以看到加载态同时考虑了默认值查询的isFetching,并默认开启showSearch、关闭客户端filterOption(把过滤交给服务端搜索)。
第二层:@refinedev/core 的核心逻辑
packages/core/src/hooks/useSelect/index.ts 实现了真正的数据编排:
- 两个查询并行:
useMany(拉取defaultValue记录)与useList(拉取选项列表)同时发起。useMany仅在defaultValues.length > 0且enabled为 true 时启用(L286-L298); - 默认值归一化:
defaultValue统一转为数组defaultValues(L249-L251); - 搜索防抖:
onSearch用lodash/debounce包裹,debounce默认300ms;若用户提供了onSearch函数则完全采用其返回的过滤数组,否则自动生成{ field: searchField, operator: "contains", value }条件(L343-L363); - 选项合并去重:通过
uniqBy(..., "value")把主列表 options 与默认值 selectedOptions 合并,selectedOptionsOrder决定合并顺序(in-place时默认值在后,selected-first时在前)(L326-L335); - 标签解析:
getOptionLabel/getOptionValue使用lodash/get支持点分路径,也支持函数形态(L216-L236); - Overtime 计时:
useLoadingOvertime同时监听主查询与默认值查询的isFetching(L320-L324)。
测试佐证
useSelect的行为由 packages/antd/src/hooks/fields/useSelect/index.spec.ts 中的测试用例覆盖,涉及资源解析、optionLabel/optionValue、排序、过滤等场景,可作为自定义改造时的行为参照。
配套示例项目
仓库提供了两个可直接运行、对照学习的完整示例:
- examples/field-antd-use-select-basic:
useSelect基础用法(基本选项绑定、搜索、默认值等); - examples/field-antd-use-select-infinite:无限滚动加载示例,演示选项很多时如何结合分页滚动加载。
小结
useSelect是 Refine + Ant Design 组合下处理下拉选项的最高频 Hook 之一:它用一行resource声明替代了手写getList请求、状态管理与选项映射的重复劳动,并通过onSearch、sorters、filters、defaultValue、pagination等属性覆盖了搜索、排序、过滤、默认值补齐、分页等几乎全部真实业务场景。理解其"useList拉选项 +useMany补默认值 + debounce 搜索 + uniqBy 合并"的底层实现(见 packages/core/src/hooks/useSelect/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),仅供参考