深入掌握 Refine 的 useMany Hook:批量数据获取、实时订阅与进阶用法全解
【免费下载链接】refineA React Framework for building internal tools, admin panels, dashboards & B2B apps with unmatched flexibility.项目地址: https://gitcode.com/GitHub_Trending/re/refine
导读
useMany是 Refine 中用于批量获取多条记录的核心数据 Hook。本文以 useMany 官方文档 为骨架,结合仓库源码 useMany.ts 与测试用例 useMany.spec.tsx,系统讲解其工作原理、全部属性(resource、ids、queryOptions、meta、通知、实时订阅、加载超时等)、返回值与最佳实践。读完本文,你将掌握如何在 Refine 应用中高效调用getMany批量拉取数据、处理降级回退、接入实时更新,并能在自定义数据提供器中正确实现与消费getMany方法。
一、useMany 是什么:基于 TanStack Query 的批量查询封装
useMany是 Refine 对 TanStack Query 的useQuery的扩展版本,支持useQuery的全部特性并增加了 Refine 专属能力。它的定位是一次请求获取多条记录,与一次只取一条的useOne、分页列表的useList形成互补。
其核心机制在源码 useMany.ts 中清晰可见:
- 它使用从
<Refine>传入的dataProvider的getMany方法作为查询函数(query function); - 它基于提供的属性生成查询键(query key)用于缓存数据,你可以在 TanStack Query Devtools 中直接查看该查询键。
从源码可见,查询键由keys().data(...).resource(...).action("many").ids(...).params(...)链式构建,这意味着当ids、resource或meta发生变化时,查询键随之变化,useMany会自动触发新的请求——这正是"属性变化即重新拉取"的底层原理(见 useMany.ts)。
没有 getMany 时的降级策略
一个值得特别注意的设计是:如果数据提供器没有实现getMany方法,useMany会退而使用getOne方法,为每个 id 逐个发起请求。源码中的实现逻辑如下:
if (getMany) { return getMany({ resource: resource?.name || "", ids, meta }); } return handleMultiple( ids.map((id) => getOne<TQueryFnData>({ resource: resource?.name || "", id, meta }), ), );见 useMany.ts。文档明确指出这种做法不推荐,因为它会为每个 id 产生一次网络请求(N 次请求),效率远低于单次getMany。因此,当你的应用需要批量获取数据时,最好在数据提供器中实现真正的getMany方法。
二、基础用法:一个可交互的完整示例
useMany的基础用法非常简单,只需提供resource与ids两个属性。下面是文档自带的实时预览示例(源文件见 _basic-usage-live-preview.md):
import { useState } from "react"; import { useMany, HttpError } from "@refinedev/core"; interface IProduct { id: number; name: string; material: string; } const ProductList: React.FC = () => { const [ids, setIds] = useState([1, 2, 3]); const { result, query: { isLoading, isError }, } = useMany<IProduct, HttpError>({ resource: "products", ids, }); const products = result?.data ?? []; if (isLoading) { return <div>Loading...</div>; } if (isError) { return <div>Something went wrong!</div>; } return ( <div> {products.map((product) => ( <ul key={product.id}> <li key={product.id}> {product.id} - {product.name}{" "} <button onClick={() => setIds((prev) => prev.filter((id) => id !== product.id)) } > remove </button> </li> </ul> ))} <button onClick={() => { setIds((prev) => [...prev, Math.floor(Math.random() * 150) + 1]); }} > Add new product </button> </div> ); };这个示例演示了三个关键行为:
- 初始批量获取:挂载时携带
ids: [1, 2, 3]发起一次getMany请求,一次性渲染三条产品记录; - 动态移除:点击某条记录的 "remove" 按钮会从
ids数组中过滤掉对应 id,useMany检测到ids变化后自动重新请求,列表随之收缩; - 动态新增:点击 "Add new product" 会向
ids追加一个随机 id,触发新的请求并拉取该 id 对应的记录。
同时注意useMany的返回结构:query对象是 TanStack Query 的查询结果(包含isLoading、isError、data等标准字段),而result.data是被展开为数组的记录列表。测试用例 useMany.spec.tsx 也验证了result.data一定是一个数组(Array.isArray(manyResult.data)为真)。
三、完整属性(Properties)详解
resource(必填)
resource会作为参数传递给数据提供器的getMany方法。它通常是对应的 API 端点路径,但具体如何解析完全取决于你在getMany方法中的实现:
useMany({ resource: "categories", });关于如何自定义
getMany方法,参见 创建数据提供器教程。
当存在多个同名资源时,可以传入identifier代替资源的name。注意:identifier仅作为资源的主匹配键,数据提供器方法内部仍使用在<Refine>组件中定义的name。详见 Refine 组件的 identifier 说明。
ids(必填)
ids用于指定要获取哪些记录,同样会传递给getMany方法:
useMany({ ids: [1, 2, 3], });在源码中,ids的类型为BaseKey[](见 useMany.ts),BaseKey通常是string | number。请求是否执行取决于enabled: hasIds && hasResource(见 useMany.ts),即只有当ids是数组且resource存在时,请求才会真正发起。如果缺少ids或resource,源码会通过warnOnce打印出带链接的警告信息,帮助开发者快速定位问题(见 useMany.ts 与 useMany.ts)。
dataProviderName
当应用配置了多个数据提供器时,用该属性指定使用哪一个:
useMany({ dataProviderName: "second-data-provider", });源码默认值为"default"(见 useMany.ts)。内部通过pickDataProvider(identifier, dataProviderName, resources)解析出最终要使用的数据提供器,并将它写入meta.dataProviderName一并传给getMany(见 [useMany.ts](https://link.gitcode.com/i/e42bc478df3747128178a94e655ef672#L135-L139, L170-L173)),因此你的getMany实现也能感知到当前使用的是哪个数据提供器。
queryOptions
queryOptions用于向底层useQuery传递额外的配置选项:
useMany({ queryOptions: { retry: 3, enabled: false, }, });从源码看,queryOptions的类型是MakeOptional<UseQueryOptions<...>, "queryKey" | "queryFn">(见 useMany.ts),即queryKey和queryFn由 Refine 内部接管,开发者不能覆盖,其余 TanStack Query 选项均可透传。若你手动把enabled设为true,即使缺少ids或resource,请求也会被强制触发(源码中的manuallyEnabled分支就是为这种场景设计的)。
更多选项参考 TanStack Query 的 useQuery 文档。
meta
meta是一个特殊属性,用于向数据提供器方法传递额外信息,典型用途包括:
- 针对特定场景定制数据提供器行为;
- 使用普通 JavaScript 对象(JSON)生成 GraphQL 查询。
下面的示例通过meta向getMany方法传递自定义请求头:
import { stringify } from "query-string"; useMany({ // highlight-start meta: { headers: { "x-meta-data": "true" }, }, // highlight-end }); const myDataProvider = { //... getMany: async ({ resource, ids, // highlight-next-line meta, }) => { // highlight-next-line const headers = meta?.headers ?? {}; const url = `${apiUrl}/${resource}?${stringify({ id: ids })}`; //... //... // highlight-next-line const { data } = await httpClient.get(`${url}`, { headers }); return { data, }; }, //... };在源码层面,meta会经过useMeta()与资源级元数据合并得到combinedMeta(见 useMany.ts),并在调用getMany时连同查询上下文(prepareQueryContext,其中包含分页、排序、过滤等查询参数)一起传入:
const meta = { ...combinedMeta, ...prepareQueryContext(context as any), };见 useMany.ts。测试用例 useMany.spec.tsx 专门验证了 hook 参数中的meta会被完整透传给数据提供器。
更多说明参见 General Concepts 文档的 meta 概念。
successNotification / errorNotification
这两个属性用于自定义成功与失败时的通知,需要配合NotificationProvider使用:
useMany({ successNotification: (data, ids, resource) => { return { message: `${data.title} Successfully fetched.`, description: "Success with no errors", type: "success", }; }, }); useMany({ errorNotification: (data, ids, resource) => { return { message: `Something went wrong when getting ${data.id}`, description: "Error", type: "error", }; }, });对应实现细节:
- 成功通知:请求成功后,若
successNotification是函数则以(queryResponse.data, ids, identifier)为参数调用,再交给handleNotification(见 useMany.ts); - 失败通知:请求失败时先调用
checkError(error)触发鉴权错误处理(如登出跳转),再发送错误通知;默认消息格式为`Error (status code: ${statusCode})`,并带有${ids[0]}-${identifier}-getMany-notification这样的去重键,防止同一请求反复弹错(见 useMany.ts)。
liveMode / onLiveEvent / liveParams(实时更新)
这三个属性依赖LiveProvider。useMany挂载时会调用liveProvider的subscribe方法订阅指定频道,从而实现数据实时刷新。
liveMode:决定收到相关实时事件时是否自动更新数据,取值为"auto"(自动)或"manual"(手动):
useMany({ liveMode: "auto", });onLiveEvent:订阅到达新事件时执行的回调:
useMany({ onLiveEvent: (event) => { console.log(event); }, });liveParams:透传给liveProvider的subscribe方法的额外参数。
源码中的订阅逻辑位于 useMany.ts:订阅频道为resources/${resource?.name ?? ""},订阅类型为"*",并在params中携带ids、meta、subscriptionType: "useMany"以及你传入的liveParams。
实时能力仅在配置了 Live Provider 时可用。
overtimeOptions(加载超时检测)
当你希望请求耗时过长时展示加载提示,可以使用overtimeOptions。interval是检测间隔(毫秒),onInterval是每个间隔触发的回调:
const { overtime } = useMany({ //... overtimeOptions: { interval: 1000, onInterval(elapsedInterval) { console.log(elapsedInterval); }, }, }); console.log(overtime.elapsedTime); // undefined, 1000, 2000, 3000 4000, ... // 使用示例:超过 4 秒就给出提示 { elapsedTime >= 4000 && <div>this takes a bit longer than expected</div>; }源码中该能力由useLoadingOvertimeHook 提供,其内部以queryResponse.isFetching作为"正在加载"的判定依据,请求完成后elapsedTime恢复为undefined(见 useMany.ts)。
四、返回值(Return Values)
useMany返回 TanStack QueryuseQuery的全部返回值,并额外提供result与overtime:
const { query, result, overtime } = useMany();query:类型为QueryObserverResult<{ data: TData[]; error: TError }>,包含isLoading、isError、isSuccess、data、error等标准字段,可直接与 TanStack Query 生态(如 Devtools、isFetching)配合;result:Refine 提供的便捷结构,result.data是TData[]数组。源码中当查询未完成时它返回Object.freeze([])冻结的空数组,保证类型安全与不变性(见 [useMany.ts](https://link.gitcode.com/i/e42bc478df3747128178a94e655ef672#L88, L261-L267));overtime:{ elapsedTime?: number },配合上面的overtimeOptions使用。
五、TypeScript 泛型参数
useMany支持三个泛型参数,用于获得完整的类型推导:
| 参数 | 说明 | 类型 | 默认值 |
|---|---|---|---|
TQueryFnData | 查询函数返回的结果数据类型,继承自BaseRecord | BaseRecord | BaseRecord |
TError | 自定义错误对象,继承自HttpError | HttpError | HttpError |
TData | select函数返回的数据类型,继承自BaseRecord;未指定时默认为TQueryFnData | BaseRecord | TQueryFnData |
典型用法:
useMany<IProduct, HttpError>({ resource: "products", ids, });六、源码视角:useMany 的完整执行链路
综合 useMany.ts 的完整实现,一次useMany调用的执行链路可以归纳为以下几步:
- 资源解析:
useResourceParams根据传入的resource解析出最终资源名与identifier; - 数据提供器选择:
useDataProvider+pickDataProvider确定使用哪个数据提供器,同时解析出getMany与getOne两个方法; - 实时订阅注册:
useResourceSubscription以resources/${resource}为频道发起订阅(需 Live Provider); - 发起查询:
useQuery使用链式构建的查询键,queryFn优先调用getMany,缺失时回退为逐 id 调用getOne;请求仅在ids有效且resource存在时启用; - 通知与错误处理:成功后触发
successNotification,失败后先走useOnError(鉴权错误处理)再触发errorNotification; - 超时检测:
useLoadingOvertime监听isFetching输出overtime.elapsedTime。
这条链路在 useMany.spec.tsx 的测试中被逐一验证,包括与 REST JSON 服务器集成(返回两条数据)、result.data的数组形态、meta的透传等场景,可作为你理解行为与编写自定义数据提供器时的参照。
七、最佳实践小结
- 优先实现
getMany:只要业务中存在批量取数,就在数据提供器中实现getMany,避免useMany降级为 N 次getOne请求; - 善用
ids的响应式:把ids放进useState(如本文示例),增删 id 即可驱动自动重新请求,无需手动调用刷新; - 用
queryOptions.enabled控制时机:如页面尚未拿到 id 列表时,用enabled: false阻止请求,待数据就绪再置为true; - 用
meta透传业务上下文:请求头、GraphQL 片段等与单次请求绑定的信息放入meta,保持数据提供器方法签名稳定; - 实时场景打开
liveMode: "auto":配合 Live Provider 让表格/详情页在多端协同编辑场景下自动更新; - 超时提示用
overtimeOptions:以interval细粒度控制"加载过久"的提示时机,提升弱网环境下的用户体验。
【免费下载链接】refineA React Framework for building internal tools, admin panels, dashboards & B2B apps with unmatched flexibility.项目地址: https://gitcode.com/GitHub_Trending/re/refine
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考