Refine 中如何用 useTable + useMany 处理关系型数据:关联表字段的完整实战指南
2026/9/11 4:31:02 网站建设 项目流程

Refine 中如何用 useTable + useMany 处理关系型数据:关联表字段的完整实战指南

【免费下载链接】refineA React Framework for building internal tools, admin panels, dashboards & B2B apps with unmatched flexibility.项目地址: https://gitcode.com/GitHub_Trending/re/refine

在 Refine 构建后台管理界面时,最常见的场景之一就是列表页展示关系型字段——例如文章列表需要同时显示每篇文章所属的分类名称,而分类数据存储在另一个资源(resource)中。本文以 Refine 官方文档useTable的 "How can I handle relational data?" 章节及其 live preview 示例为骨架,结合packages/coreuseTableuseMany的真实源码实现,完整讲解"列表 + 关联数据"的标准解法:用useTable拉取主表数据,用useMany按 ids 批量拉取关联资源,再在渲染层完成关联匹配。读完本文,你将掌握一套可直接复制的关联数据列表页写法,并理解其背后的请求合并与缓存原理。

一、问题背景:列表页中的关联字段从哪来

假设你的 API 返回的文章(posts)数据结构如下:文章对象中只携带了分类的外键category.id,而分类的完整信息(如title)存放在独立的categories资源中:

interface IPost { id: number; title: string; content: string; status: "published" | "draft" | "rejected"; createdAt: string; // 只存外键,不内嵌完整分类对象 category: { id: number; }; } interface ICategory { id: number; title: string; }

如果直接在表格中渲染post.category,你只能得到{ id: 2 }这样的外键引用,无法展示分类名称。要解决这个问题,需要两步:

  1. useTable获取当前页的文章列表;
  2. 从文章列表中提取所有category.id,用useMany一次性批量获取对应的分类记录;
  3. 渲染时在分类结果中按id找到每篇文章的分类标题。

这正是 Refine 官方文档中 FAQ 章节"如何处理关系型数据"推荐的做法(见 useTable 文档 与对应的 live preview 示例 _partial-relational-data-live-preview.md)。

二、核心解法:useTable + useMany 组合模式(完整可运行代码)

下面是官方示例的完整代码。它在一个 headless 列表页中完成"文章 + 分类名称"的关联渲染,无任何 UI 库依赖,可直接套用到自己的项目中:

import React from "react"; import { useTable, // 处理关系型数据的关键 hook useMany, HttpError, } from "@refinedev/core"; interface ICategory { id: number; title: string; } interface IPost { id: number; title: string; content: string; status: "published" | "draft" | "rejected"; createdAt: string; category: { id: number; }; } const PostList: React.FC = () => { const { result, tableQuery } = useTable<IPost, HttpError>(); const posts = result.data; // 核心逻辑:根据文章列表中的 category.id,批量拉取分类数据 const { result: categoryData, query: { isLoading: categoryIsLoading }, } = useMany<ICategory, HttpError>({ resource: "categories", // 从文章列表中提取所有分类 id,一次性请求 ids: posts.map((item) => item?.category?.id), queryOptions: { // 仅在文章列表非空时才发起请求,避免空数组请求 enabled: !!posts.length, }, }); if (tableQuery?.isLoading) { return <div>Loading...</div>; } return ( <div> <h1>Posts</h1> <table> <thead> <tr> <th>ID</th> <th>Title</th> <th>Status</th> <th>Created At</th> <th>Category</th> </tr> </thead> <tbody> {posts.map((post) => ( <tr key={post.id}> <td>{post.id}</td> <td>{post.title}</td> <td>{post.status}</td> <td>{new Date(post.createdAt).toDateString()}</td> <td> {categoryIsLoading ? "loading..." : // 在分类结果中按 id 找到当前文章的分类标题 categoryData?.data.find( (item) => item.id === post.category.id, )?.title || "-"} </td> </tr> ))} </tbody> </table> </div> ); };

这段代码的三个关键点值得逐条拆解。

2.1 用 useTable 获取主表数据

useTable<IPost, HttpError>()负责根据当前路由的resource(默认从 URL 解析,参见 useTable 源码 中的useResourceParams调用)拉取文章列表。它返回两个核心对象:

  • tableQuery:TanStack Query 的查询结果对象,tableQuery.isLoading表示列表是否加载中;
  • result:便捷的数据访问层,result.data是当前页记录数组,result.total是总记录数。在关联数据场景中,useMany只关心本页的 ids,因此分页信息无需额外处理。

从源码看,useTable内部把分页、排序、过滤状态统一合并后交给了useList处理:

const queryResult = useList<TQueryFnData, TError, TData>({ resource: identifier, pagination: { currentPage: currentPage, pageSize, mode: pagination?.mode }, filters: isServerSideFilteringEnabled ? unionFilters(preferredPermanentFilters, filters) : undefined, sorters: isServerSideSortingEnabled ? unionSorters(preferredPermanentSorters, sorters) : undefined, // ... });

这意味着:表格每次翻页、排序或过滤后重新请求,result.data都会更新为当前页数据,而useManyids依赖posts.map(...)也会随之变化并自动重新请求,整个关联链路是响应式的。

2.2 用 useMany 批量拉取关联数据

useMany接受两个必填参数:resource(关联资源名)和ids(要获取的记录 id 数组)。这里ids来自对文章列表的映射:

ids: posts.map((item) => item?.category?.id),

posts变化(如翻页)时,ids变化,useMany会自动触发新的请求。这一点在 useMany 文档 中明确说明:"When these properties are changed, theuseManyhook will trigger a new request."

2.3 用 enabled 守卫避免空请求

queryOptions: { enabled: !!posts.length, },

这是一个非常重要的细节:当文章列表为空(比如筛选无结果、第一帧数据未到达)时,ids是空数组,如果照常发起请求会白白浪费一次网络调用。通过 TanStack Query 的enabled: false让查询进入"禁用"状态,直到posts非空才自动启动。这也是示例注释中点名的 "Set to true only if the posts array is not empty"。

三、深入源码:useMany 底层是如何工作的

理解了用法之后,再看 useMany 源码 能帮你建立更扎实的心智模型。该 hook 是 TanStack QueryuseQuery的封装,它有四个核心行为:

1. 以getMany作为查询函数。useMany的 JSDoc 注释明确指出:"It usesgetManymethod as query function from thedataProviderwhich is passed to<Refine>." 也就是说,最终发出的请求由你的dataProvidergetMany方法决定——对 REST 类 provider,通常对应GET /categories?ids=1&ids=2这样的批量接口。

2. 存在 getMany 缺省时的降级策略。如果你的dataProvider没有实现getManyuseMany会退回使用getOne方法逐个请求每个 id。这虽然能保证功能可用,但会产生 N 次请求,性能较差。官方文档明确建议:"It is better to implement thegetManymethod in the data provider."(见 use-many/index.md)

3. 基于属性生成查询键实现缓存。查询键由传入的resourceidsmeta等属性组合生成,因此相同的 ids 组合会被 TanStack Query 缓存复用。翻回上一页时,同样的分类集合直接命中缓存,不会重复请求。

4. 返回queryresult双结构。useTable类似,useMany同时暴露:

  • query:完整的 TanStack Query 结果(含isLoadingisErrordata等);
  • result.data:本次批量获取到的记录数组,示例中的categoryData?.data即此数据。

对应到源码中的返回类型定义:

export type UseManyReturnType<TData, TError> = { query: QueryObserverResult<GetManyResponse<TData>, TError>; result: { data: TData[]; }; } & UseLoadingOvertimeReturnType;

四、useMany 关键参数详解

关系型数据场景中,useMany的常用参数如下(完整列表见 use-many/index.md):

参数必填说明
resource关联资源名,会传给getMany方法,通常作为 API 端点路径
ids要获取的记录 id 数组,类型为BaseKey[]
queryOptionsTanStack Query 的useQuery配置,如enabledretry;示例中用于空列表守卫
dataProviderName存在多个 dataProvider 时指定使用哪一个
meta传递给 dataProvider 的附加信息,如自定义 headers、GraphQL 查询字段
liveMode/onLiveEvent配合 LiveProvider 实现实时更新
overtimeOptions请求超时提示,interval毫秒间隔回调onInterval

特别说明meta的用途:它最常见的场景是给getMany传自定义请求头,或用于 GraphQL 数据源生成查询。例如:

useMany({ resource: "categories", ids: [1, 2, 3], meta: { headers: { "x-meta-data": "true" }, }, });

对应到自定义dataProvider中,meta会作为getMany的参数之一被消费:

const myDataProvider = { // ... getMany: async ({ resource, ids, meta }) => { const headers = meta?.headers ?? {}; const url = `${apiUrl}/${resource}?${stringify({ id: ids })}`; const { data } = await httpClient.get(`${url}`, { headers }); return { data }; }, };

五、最佳实践与注意事项

5.1 务必实现 dataProvider 的 getMany

如第三节所述,getMany缺失时useMany会退化为逐条getOne。在分类数量较大的列表页(每页 10~50 条记录)上,这会产生同样数量的请求。正确的做法是在 REST 类 dataProvider 中实现批量接口,例如 Refine 的@refinedev/simple-rest@refinedev/rest等官方 provider 都原生支持getMany,直接使用即可。

5.2 给空列表加 enabled 守卫

queryOptions: { enabled: !!posts.length, },

这行代码虽小,却能避免两类问题:首帧加载时posts为空导致的无效请求,以及筛选无结果时对空 ids 的请求。

5.3 渲染层的容错处理

示例中使用了"三保险"渲染策略:

{categoryIsLoading ? "loading..." : categoryData?.data.find((item) => item.id === post.category.id)?.title || "-"}
  • categoryIsLoading:关联数据加载中显示占位文案;
  • ?.|| "-":分类结果中找不到对应 id(如数据已被删除)时优雅降级,避免页面崩溃。

5.4 与客户端过滤/排序的组合

如果你把useTablefilters.modesorters.mode设为"off"(客户端模式),result.data仍是服务端返回的原始数组,此时useMany的 ids 应基于过滤/排序后的数组计算。建议结合useMemoresult.data做派生后再 map ids,确保关联请求只针对真正展示的记录。

5.5 关联数据也有超时提示

useManyuseTable都支持overtimeOptions。当关联资源响应较慢时,可以通过elapsedTime给出"加载时间过长"的提示:

const { overtime } = useMany({ resource: "categories", ids: [1, 2, 3], overtimeOptions: { interval: 1000, onInterval(elapsedInterval) { console.log(elapsedInterval); }, }, }); // 渲染层 { overtime.elapsedTime >= 4000 && <div>this takes a bit longer than expected</div> }

六、总结

关系型数据是后台列表页的"标配需求"。Refine 给出的标准解法非常清晰:

  1. useTable负责主列表:它基于排序、过滤、分页状态请求当前页数据(内部经useList调用getList);
  2. useMany负责关联数据:从主列表提取外键 ids,经getMany一次批量请求,并由查询键自动缓存复用;
  3. 渲染层匹配:在分类结果中find对应记录,配以加载态与兜底值。

这套模式在 useTable 文档的 FAQ 章节 有官方示例,在 packages/core/src/hooks/data/useMany.ts 与 packages/core/src/hooks/useTable/index.ts 中有完整实现可查。掌握它,你就能在任意 Refine 项目中高效、可维护地展示关联字段,同时避免 N+1 请求陷阱。

【免费下载链接】refineA React Framework for building internal tools, admin panels, dashboards & B2B apps with unmatched flexibility.项目地址: https://gitcode.com/GitHub_Trending/re/refine

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询