Refine useTable 关联数据实战:用 useMany 在列表表格中批量拉取外键对应的记录(v3 API)
2026/9/14 10:13:26 网站建设 项目流程

Refine useTable 关联数据实战:用 useMany 在列表表格中批量拉取外键对应的记录(v3 API)

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

本文以 Refine v3(@pankod/refine-core)文档中useTable页面的 FAQ 示例为骨架,完整讲解列表页的"关联数据"问题:当表格记录的某个字段只存了外键 id(例如文章只存了category.id),如何通过useManyHook 一次性批量拉取关联资源(分类)的完整数据并渲染到表格中。读完本篇,你将掌握useTableuseMany的标准组合方式、queryOptions.enabled的条件查询技巧,以及从 Refine 源码层面理解useMany的缓存键生成与getMany/getOne降级机制。

问题场景:表格列需要展示关联资源的字段

Refine 的useTable是一个 headless Hook:它只根据排序、过滤、分页状态从当前资源的端点取数据,本身不会帮你 JOIN 其他资源。典型的数据模型是:

  • 资源posts的每行数据里,category字段只有{ id }(外键),没有title
  • 表格却需要在 "Category" 列显示分类标题。

v3 官方文档在useTable页面的 FAQ("How can I handle relational data?")给出的答案就是:useMany按当前页所有行的外键 id 批量拉取categories资源,再在前端做 id 映射。下文完整继承该 FAQ 的官方示例代码并逐段拆解。

完整示例代码(继承自 v3 官方 FAQ 示例)

下面这段代码来自 useTable 关联数据示例,运行在/posts路由上,渲染一个包含 ID、Title、Status、Created At、Category 五列的表格:

import React from "react"; import { IResourceComponentsProps, useTable, // 用于批量拉取关联数据 useMany, HttpError, } from "@pankod/refine-core"; interface ICategory { id: number; title: string; } interface IPost { id: number; title: string; content: string; status: "published" | "draft" | "rejected"; createdAt: string; // 关联字段只保留了外键 id category: { id: number; }; } const PostList: React.FC<IResourceComponentsProps> = () => { const { tableQueryResult } = useTable<IPost, HttpError>(); const posts = tableQueryResult?.data?.data ?? []; // Fetches the category of each post. It uses the useMany hook to fetch the category data from the API. const { data: categoryData, isLoading: categoryIsLoading } = useMany< ICategory, HttpError >({ resource: "categories", // Creates the array of ids. This will filter and fetch the category data for the relevant posts. ids: posts.map((item) => item?.category?.id), queryOptions: { // Set to true only if the posts array is not empty. enabled: !!posts.length, }, }); if (tableQueryResult?.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..." : // Gets the title of the category from the categoryData object, which is the result of the useMany hook. categoryData?.data.find( (item) => item.id === post.category.id, )?.title || "-"} </td> </tr> ))} </tbody> </table> </div> ); };

官方 live preview 的入口部分(注册资源并渲染演示组件)在文档站中通过setRefinePropsPostList挂到resources: [{ name: "posts", list: PostList }]上,最终渲染<RefineHeadlessDemo />;这些是文档站自身的脚手架调用,实际项目里只需把PostList作为posts资源的list组件注册到<Refine>即可。

逐段拆解:这个组合模式的四个关键点

1. 先用 useTable 拿到当前页数据

const { tableQueryResult } = useTable<IPost, HttpError>(); const posts = tableQueryResult?.data?.data ?? [];

v3 中useTable内部通过useList完成请求(见 useTable 文档),返回tableQueryResult(TanStack Query 的useQuery结果)。这里把data兜底为空数组,保证后续posts.map在请求未回来时不会报错。

2. ids:从当前页记录中提取外键集合

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

useMany只接受两个核心入参:resourceids(见 useMany 文档)。把当前页所有行的category.id收集成数组传入,意味着只请求本页需要的分类——翻页或过滤后ids变化,useMany会触发新一轮请求,关联数据始终与表格数据保持同步。

3. queryOptions.enabled:空列表时跳过请求

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

这是示例里容易忽略但很实用的细节:当posts为空(比如过滤条件命中 0 条记录)时,ids是一个空数组,此时让查询处于 disabled 状态,避免发出无意义的GET categories请求。

从源码结构看,这一机制在useMany内部有双重保障。useMany 实现 中,查询默认以enabled: hasIds && hasResource开启,随后...queryOptions展开允许业务侧覆盖enabled。也就是说,官方示例的enabled: !!posts.length正是利用queryOptions覆盖默认行为。

4. 渲染:loading 态 + find 映射 + 兜底值

{categoryIsLoading ? "loading..." : categoryData?.data.find((item) => item.id === post.category.id)?.title || "-"}

关联数据与主数据是两条独立的查询链,加载时机不同:主表格等待tableQueryResult.isLoading,分类列等待categoryIsLoading。渲染时对每个 post 在categoryData.data中按id === post.category.id查找标题,找不到时回退为"-",避免脏外键导致整列崩溃。

源码视角:useMany 是怎么工作的

示例之所以可以放心地"翻页后自动重新拉取",根因在 useMany 的源码:

  1. 查询键(queryKey)包含 idskeys().data(pickedDataProvider).resource(identifier).action("many").ids(...(ids ?? [])).params(...).get()(useMany.ts#L176-L189)。ids 变化会生成新 queryKey,从而触发新请求;相同 ids 则命中 TanStack Query 缓存,翻页回原页时基本零成本。
  2. 优先走 dataProvider 的getManyqueryFn内先判断if (getMany),调用getMany({ resource, ids, meta })(useMany.ts#L196-L201)。
  3. getMany时降级为逐个getOne:若 dataProvider 未实现getManyuseMany会用handleMultiple对每个 id 并发调用getOne(useMany.ts#L203-L211)。这与 useMany 文档中的 caution 一致:能批量取就不要一条一条取,建议在 dataProvider 中实现getMany
  4. 返回值结构:返回{ query, result },其中result.data是数组,无数据时回退为冻结空数组(useMany.ts#L261-L267),因此前端可以放心地直接.find
  5. 实时订阅:Hook 挂载时还会调用useResourceSubscription向 LiveProvider 订阅resources/{resource}频道(useMany.ts#L157-L174),配置了 LiveProvider 的分类数据更新同样可以实时刷新。

版本提示:v3 与新版返回结构差异

本文示例严格对应 v3(@pankod/refine-core)的 API:useTable返回tableQueryResult,示例中以tableQueryResult?.data?.data读取行数组。需要说明的是,仓库当前主版本(v4/v5,包名@refinedev/core)对 Hook 返回值做了重命名:从 当前 useMany 源码 可以确认新版useMany返回queryresultresult.data为数组),而新版useTable文档将对应返回项标记为tableQuery/result(参见 新版 useTable 文档)。如果你在维护 v5 项目,"useTable 提取 ids → useMany 批量拉取关联 → find 映射渲染"的整体模式完全一致,只需按新版返回结构调整取数字段即可。

适用边界与小结

  • 适用:关联记录数量有限、且后端没有提供聚合/JOIN 端点时,前端批量映射是低成本方案;queryOptions.enabled+ 缓存键机制保证了空列表不发请求、重复 ids 不重复请求。
  • 边界useManyids规模受限于单页记录数(本例默认pageSize为 10),所以该模式天然与分页配合良好;若单页数据量很大,应从后端聚合端点解决,而不是继续放大ids数组。
  • 一句话总结useTable负责"当前页主数据 + 排序/过滤/分页状态",useMany负责"按外键 id 批量补齐关联数据",两者通过posts.map((item) => item?.category?.id)这一行代码衔接,是 Refine 处理列表页关联展示的标准姿势。

延伸阅读(仓库内路径)

  • useTable v3 文档(FAQ 来源页)
  • useMany v3 文档
  • useMany 源码实现
  • useTable 关联数据示例原文

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

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

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

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

立即咨询