news 2026/9/10 10:48:18

Refine 数据获取实战:useList Hook 的选项、模式与边界场景全解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Refine 数据获取实战:useList Hook 的选项、模式与边界场景全解析

Refine 数据获取实战:useList Hook 的选项、模式与边界场景全解析

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

useList是 Refine 核心包(@refinedev/core)中最常用的数据获取 Hook,它建立在 TanStack Query 的useQuery之上,为列表类资源的分页、排序、过滤、实时更新与超时反馈提供了开箱即用的能力。本文以 Refine 仓库中 useList 官方文档 为主体,结合 useList 源码、单元测试 与 simple-rest 数据提供器 的实现细节,带你从 API 用法一路深入到查询键(query key)生成、客户端分页切片、通知处理与实时订阅的底层原理,帮助你写出可扩展、可维护的列表页面。

useList 是什么:useQuery 的“列表化”扩展

从定义上看,useList是 TanStack QueryuseQuery的扩展版本——它完整继承了useQuery的全部能力(缓存、重试、enabledselect等),并在此基础上增加了 Refine 特有的列表语义:

  • 查询函数(query function):内部调用dataProvidergetList方法作为查询函数。getList接收resourcepaginationsortersfiltersmeta等参数,返回{ data, total }
  • 查询键(query key):由 Hook 传入的属性自动生成,用于缓存数据。你可以借助 TanStack Query Devtools 直接观察到这个键的完整结构。

从源码看,useList的返回结构被设计为{ query, result, overtime }三部分(见 useList.ts):

export type UseListReturnType<TData, TError> = { query: QueryObserverResult<GetListResponse<TData>, TError>; result: { data: TData[]; total: number | undefined; [key: string]: any; }; } & UseLoadingOvertimeReturnType;

其中query就是原生的useQuery返回值(包含isLoadingisErrorisFetching等状态),result则提供了便捷的data(无数据时为空数组)与total访问方式,overtime用于追踪请求耗时。

基本用法:从零渲染一个产品列表

最基础的用法只需传入resource即可。以下示例来自仓库中的 _basic-usage-live-preview.md,完整展示了 Hook 的接入方式:

import { useList, HttpError } from "@refinedev/core"; interface IProduct { id: number; name: string; material: string; } const ProductList: React.FC = () => { const { result, query } = useList<IProduct, HttpError>({ resource: "products", }); const products = result.data ?? []; if (query.isLoading) { return <div>Loading...</div>; } if (query.isError) { return <div>Something went wrong!</div>; } return ( <ul> {products.map((product) => ( <li key={product.id}> <h4> {product.name} - ({product.material}) </h4> </li> ))} </ul> ); };

几个值得注意的细节:

  • 泛型参数useList<IProduct, HttpError>中的第一个类型参数是查询函数返回的记录类型,第二个是自定义错误类型(需继承HttpError)。它们与 TanStack Query 的TQueryFnDataTError一一对应。
  • result.data兜底:源码中result.data在没有数据时会回退到EMPTY_ARRAY(一个被冻结的空数组),因此products.map(...)无需担心undefined
  • 状态判定query.isLoading/query.isError均来自 TanStack Query 的useQuery返回值,用法与原生一致。

分页(Pagination):三种模式与 total 的来源

useList通过pagination属性启用分页,并原样透传给getList动态修改pagination属性会触发新的请求。其类型为:

pagination?: { currentPage?: number; pageSize?: number; mode?: "off" | "client" | "server"; };

默认值与参数归一化

当你不传pagination时,内部会通过handlePaginationParams补齐默认值(见 handlePaginationParams/index.ts):

  • mode默认为"server"
  • currentPage默认为1
  • pageSize默认为10

这意味着即使你完全省略pagination属性,getList收到的仍是一份完整的{ currentPage: 1, pageSize: 10, mode: "server" }

mode 三种取值的语义

取值含义分页行为
"server"服务端分页(默认)pagination作为查询参数传给getList,并由其拼接到 API 请求中
"client"客户端分页一次性拉取全量数据,在浏览器端对返回数组做切片
"off"关闭分页不分页,一次取回所有数据

客户端分页的实现值得展开:在 useList.ts 中,memoizedSelect会在mode === "client"时对data.data做本地切片:

if (prefferedPagination.mode === "client") { data = { ...data, data: data.data.slice( (prefferedPagination.currentPage - 1) * prefferedPagination.pageSize, prefferedPagination.currentPage * prefferedPagination.pageSize, ), total: data.total, }; }

也就是说,客户端分页是借助useQueryselect机制实现的,切片逻辑发生在查询结果被消费之前。

total 的检索方式与 rowCount 约定

getList被调用时,Refine 期望返回结果中包含总行数(total/rowCount)。不同数据提供器的获取方式各不相同:

  • REST 类提供器:通常读取响应头中的x-total-count
  • GraphQL 类提供器:通常从特定字段读取,例如pageInfo.total
  • 其他提供器:遵循各自约定的方式。
  • 兜底策略:如果后端没有提供总数,getList可以回退为返回数组的长度作为total

这一点在仓库中有直接证据。以 simple-rest 的getList为例:

getList: async ({ resource, pagination, filters, sorters, meta }) => { const url = `${apiUrl}/${resource}`; const { currentPage = 1, pageSize = 10, mode = "server" } = pagination ?? {}; // ... if (mode === "server") { query._start = (currentPage - 1) * pageSize; query._end = currentPage * pageSize; } // ... const { data, headers } = await httpClientrequestMethod; const total = +headers["x-total-count"]; return { data, total: total || data.length, // 无 header 时回退为数组长度 }; },

可以看到:服务端分页模式下,simple-rest 会把分页参数转换为 JSON Server 风格的_start/_end查询参数;总数优先取x-total-count响应头,缺失时退化为data.length。这一约定与 getList 文档 中给出的参考实现完全一致:

getList: async ({ resource, pagination, sorters, filters, meta }) => { const { currentPage, pageSize } = pagination ?? {}; const response = await apiClient.get(`/${resource}`, { params: { _page: currentPage, _limit: pageSize }, }); const total = response.headers["x-total-count"] ?? response.data.length; return { data: response.data, total }; };

一个完整的分页交互示例

来自 _pagination-live-preview.md 的示例展示了如何用 React 状态驱动currentPagepageSize,改动任一状态都会触发getList重新请求:

import { useState } from "react"; import { useList, HttpError } from "@refinedev/core"; interface IProduct { id: number; name: string; material: string; } const ProductList: React.FC = () => { const [currentPage, setCurrentPage] = useState(1); const [pageSize, setPageSize] = useState(5); const { result, query } = useList<IProduct, HttpError>({ resource: "products", pagination: { currentPage, pageSize, }, }); const products = result.data ?? []; if (query.isLoading) { return <div>Loading...</div>; } if (query.isError) { return <div>Something went wrong!</div>; } return ( <div> <button onClick={() => setCurrentPage((prev) => prev - 1)}>{"<"}</button> <span> page: {currentPage} </span> <button onClick={() => setCurrentPage((prev) => prev + 1)}>{">"}</button> <span> per page: </span> <select value={pageSize} onChange={(e) => setPageSize(Number(e.target.value))}> {[5, 10, 20].map((size) => ( <option key={size} value={size}> {size} </option> ))} </select> <ul> {products.map((product) => ( <li key={product.id}> <h4> {product.name} - ({product.material}) </h4> </li> ))} </ul> </div> ); };

查询键如何受分页模式影响

useList的查询键由useKeys生成(见 useList.ts),结构大致为:

["data", dataProviderName, resourceName, "list", { meta, filters, pagination?, sorters? }]

一个关键行为是:只有服务端分页(mode === "server")才会把pagination写进查询键。这在 useList.spec.tsx 的测试中有明确验证:

  • mode"server"(或未指定,默认即 server)时,getList收到的meta.queryKey中包含pagination: { currentPage, mode, pageSize }
  • mode"client""off"时,meta.queryKey不包含pagination,因为服务端不需要按页码请求,改动分页不应产生新的网络请求。

排序(Sorting):sorters 属性

useList支持通过sorters属性排序,并原样透传给getList动态修改sorters会触发新的请求。其类型为CrudSort[],即{ field: string; order: "asc" | "desc" }的数组。

useList({ sorters: [ { field: "title", order: "asc", }, ], });

来自 _sorting-live-preview.md 的交互示例展示了通过按钮在asc/desc之间切换排序:

import { useState } from "react"; import { useList, HttpError } from "@refinedev/core"; interface IProduct { id: number; name: string; material: string; } const ProductList: React.FC = () => { const [order, setOrder] = useState<"asc" | "desc">("asc"); const { result, query } = useList<IProduct, HttpError>({ resource: "products", sorters: [ { field: "name", order, }, ], }); const products = result.data ?? []; if (query.isLoading) { return <div>Loading...</div>; } if (query.isError) { return <div>Something went wrong!</div>; } return ( <div> <button onClick={() => setOrder((prev) => (prev === "asc" ? "desc" : "asc"))}> toggle sort </button> <ul> {products.map((product) => ( <li key={product.id}> <h4> {product.name} - ({product.material}) </h4> </li> ))} </ul> </div> ); };

在 simple-rest 提供器中,sorters会被generateSort转换成 JSON Server 风格的_sort/_order查询参数(见 provider.ts)。更详细的字段语义可参考 CrudSorting 接口定义。

过滤(Filtering):filters 属性

useList通过filters属性支持过滤,并原样透传给getList动态修改filters会触发新的请求。其类型为CrudFilter[],每个过滤条件形如{ field, operator, value }

useList({ filters: [ { field: "title", operator: "contains", value: "Foo", }, ], });

来自 _filtering-live-preview.md 的示例演示了通过下拉框切换过滤值(material等于Cotton/Bronze/Plastic):

import { useState } from "react"; import { useList, HttpError } from "@refinedev/core"; interface IProduct { id: number; name: string; material: string; } const ProductList: React.FC = () => { const [value, setValue] = useState("Cotton"); const { result, query } = useList<IProduct, HttpError>({ resource: "products", filters: [ { field: "material", operator: "eq", value, }, ], }); const products = result.data ?? []; if (query.isLoading) { return <div>Loading...</div>; } if (query.isError) { return <div>Something went wrong!</div>; } return ( <div> <span> material: </span> <select value={value} onChange={(e) => setValue(e.target.value)}> {["Cotton", "Bronze", "Plastic"].map((material) => ( <option key={material} value={material}> {material} </option> ))} </select> <ul> {products.map((product) => ( <li key={product.id}> <h4> {product.name} - ({product.material}) </h4> </li> ))} </ul> </div> ); };

operator支持eqnecontainsgtgteltltebetweeninandor等多种语义,具体字段定义参见 CrudFilters 接口。在 simple-rest 提供器中,filtersgenerateFilter转换为 URL 查询参数后与分页、排序参数合并(见 provider.ts)。

实时更新(Realtime Updates):订阅与三种 live 属性

该功能仅在配置了 Live Provider 时可用。

useList挂载时,它内部会调用liveProvidersubscribe方法,传入channelresource等参数,用于订阅实时事件。从源码看(useList.ts),订阅通过useResourceSubscription完成:

  • channelresources/${resource?.name}
  • 订阅事件类型为"*"(全部事件);
  • 订阅参数包含metapaginationhasPaginationsortersfilters,以及标识为subscriptionType: "useList",用户传入的liveParams也会被合并进去。

文档中还提到了一个值得注意的派生关系:useTableuseSelectuseInfiniteList等 Hook 内部都基于useList实现,因此它们会订阅相同的事件通道(参见 live-provider 文档 中“派生 Hook 订阅同一事件”的说明)。

useList提供以下与实时相关的属性:

属性说明示例
liveMode收到相关实时事件后,是自动更新数据("auto")还是手动处理("manual"liveMode: "auto"
onLiveEvent收到订阅事件时的回调函数onLiveEvent: (event) => console.log(event)
liveParams透传给liveProvider.subscribe方法的额外参数可自定义订阅所需的上下文
useList({ liveMode: "auto", onLiveEvent: (event) => { console.log(event); }, });

属性详解:从 resource 到 overtimeOptions

resource(必填)

resource会被作为参数传给getList。它通常对应 API 端点路径,但具体如何解析完全取决于getList的实现:

useList({ resource: "categories", });

当多个资源同名时,可以改用identifier来区分。identifier仅作为资源匹配的主键,数据提供器方法内部仍然使用<Refine>组件中定义的name工作。有关identifier的完整说明参见 Refine 组件文档。

dataProviderName

当你的应用中配置了多个数据提供器时,通过该属性指定使用哪一个:

useList({ dataProviderName: "second-data-provider", });

源码中通过pickDataProvider(identifier, dataProviderName, resources)完成选择(useList.ts),且该名称会进入查询键,保证不同提供器的缓存相互隔离。

queryOptions

queryOptions用于把额外选项透传给 TanStack Query 的useQuery,例如重试次数、enabledselectstaleTime等:

useList({ queryOptions: { retry: 3, }, });

注意两点实现细节:

  • enabled的默认逻辑:当用户未显式设置enabled时,Hook 会以!!resource?.name作为默认值,即资源名缺失时查询自动禁用(useList.ts)。
  • select的合并顺序:用户传入的select会在客户端分页切片之后执行,因此select接收到的已是切片后的数据(useList.ts)。源码注释同时提醒:如果select未被useCallback记忆化,它会在每次渲染时重新执行。

meta

meta是 Refine 中用于向数据提供器传递额外信息的特殊属性,常见用途有两种:

  1. 针对特定场景定制数据提供器的行为;
  2. 用纯 JavaScript 对象(JSON)生成 GraphQL 查询。

下面的示例在meta中传递自定义请求头,并在自定义getList中取出使用:

useList({ 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 } = await httpClient.get(`${url}`, { headers }); return { data, }; }, //... };

在 simple-rest 提供器中,meta.headers会直接作为 axios 请求头传入(见 provider.ts),这也是“携带认证令牌/自定义头”最常见的做法。关于 meta 的合并规则(来自 resource 定义、Hook 调用与上下文三处的 meta 最终会合并为一份),可参考 General Concepts 文档的 Meta Concept 章节。

successNotification 与 errorNotification

这两个属性需要配合 NotificationProvider 使用。

  • successNotification:数据获取成功后,useList会调用NotificationProvider.open展示成功通知,该属性用于定制通知内容(默认值为false,即默认不弹成功通知)。
  • errorNotification:数据获取失败后,useList会调用open展示错误通知,该属性用于定制错误内容(默认消息为"Error (status code: {statusCode})")。

两个属性都支持传入对象或回调函数:

useList({ successNotification: (data, values, resource) => { return { message: `${data.title} Successfully fetched.`, description: "Success with no errors", type: "success", }; }, }); useList({ errorNotification: (data, values, resource) => { return { message: `Something went wrong when getting ${data.id}`, description: "Error", type: "error", }; }, });

源码中的处理逻辑位于两个useEffect中(useList.ts):成功时根据successNotification配置调用handleNotification;失败时先通过useOnErrorcheckError触发鉴权错误处理,再以key: ${identifier}-useList-notification作为通知标识调用handleNotification,并用translate生成默认错误文案。

overtimeOptions

当请求耗时过长时,可以用overtimeOptions开启加载超时反馈,便于展示“请求比预期更久”的提示。其中interval是回调触发的时间间隔(毫秒),onInterval是每个间隔触发的回调。Hook 返回的overtime对象中的elapsedTime表示已耗时(毫秒),请求完成时变为undefined

const { overtime } = useList({ //... 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>; }

实现上,overtimeuseLoadingOvertimeHook 基于queryResponse.isFetching驱动(useList.ts)。

返回值(Return Values)

useList返回 TanStack QueryuseQuery的全部返回值,外加两个扩展字段:

字段类型说明
queryQueryObserverResult<{ data: TData[]; total: number }, TError>原生useQuery返回值,含isLoadingisErrorisFetchingrefetch
result{ data: TData[]; total: number; [key: string]: any }便捷结构:data为记录数组(空时为空数组),total为总数
overtime{ elapsedTime?: number }请求已耗时(毫秒),完成时为undefined
const { overtime } = useList(); console.log(overtime.elapsedTime); // undefined, 1000, 2000, 3000 4000, ...

值得注意的是result通过展开queryResponse.data并覆盖data/total构建(useList.ts),因此在getList返回了其他字段时,result也会一并透出,保持了灵活性。

类型参数(Type Parameters)

useList支持三个泛型参数,用于保障类型安全:

类型参数说明默认值
TQueryFnData查询函数返回的记录类型,需继承BaseRecordBaseRecord
TError自定义错误类型,需继承HttpErrorHttpError
TDataselect处理后返回的记录类型,同样继承BaseRecordTQueryFnData

边界场景与最佳实践小结

  • 默认分页是服务端模式:不传pagination时默认{ currentPage: 1, pageSize: 10, mode: "server" },API 会收到_start/_end这类范围参数(simple-rest 风格)。
  • 客户端分页不走网络mode: "client"时数据在本地切片,且pagination不会进入查询键,因此翻页不会触发新请求,适合数据量小、后端不支持分页的场景。
  • total 的可靠性依赖提供器getList返回的total可能来自x-total-count响应头、GraphQL 的pageInfo.total,或退化为数组长度,编写自定义提供器时要保证格式统一为{ data, total }
  • 实时订阅与派生 HookuseList挂载即订阅resources/${resource}通道;useTableuseSelectuseInfiniteList等基于它实现,会继承相同的实时行为。
  • 缓存键结构化:查询键为["data", dataProviderName, resource, "list", { meta, filters, pagination?, sorters? }],善用 TanStack Query Devtools 观察它,有助于理解缓存失效与重新请求的时机。

延伸阅读

  • Data Provider 与 getList 方法:了解getList的参数、返回值与游标分页支持
  • Refine 组件与 identifier:资源匹配键的完整语义
  • 接口参考(CrudFilters / CrudSorting / BaseRecord / HttpError):过滤器、排序器与基础类型的字段定义
  • Live Provider 文档:实时订阅的协议与派生 Hook 的订阅行为
  • Notification Provider 文档:成功/失败通知的接入方式
  • General Concepts 的 Meta 概念:meta 的三处来源与合并规则
  • useList 单元测试:查询键、客户端切片等行为的可运行验证用例

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

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/10 10:47:08

CANN/ge设置图布尔属性API

EsSetBoolAttrForGraph 【免费下载链接】ge GE&#xff08;Graph Engine&#xff09;是面向昇腾的图编译器和执行器&#xff0c;提供了计算图优化、多流并行、内存复用和模型下沉等技术手段&#xff0c;加速模型执行效率&#xff0c;减少模型内存占用。 GE 提供对 PyTorch、Ten…

作者头像 李华
网站建设 2026/9/10 10:46:17

Java泛型编程:从基础到高级应用全解析

1. 泛型基础概念与核心价值泛型&#xff08;Generics&#xff09;是Java 5引入的最重要语言特性之一&#xff0c;它允许在定义类、接口和方法时使用类型参数。这种参数化的类型机制&#xff0c;从根本上解决了容器类运行时类型转换的安全隐患。我仍记得2004年首次接触泛型时&am…

作者头像 李华
网站建设 2026/9/10 10:44:26

CANN/GE aclgrph接口文档

aclgrph接口 【免费下载链接】ge GE&#xff08;Graph Engine&#xff09;是面向昇腾的图编译器和执行器&#xff0c;提供了计算图优化、多流并行、内存复用和模型下沉等技术手段&#xff0c;加速模型执行效率&#xff0c;减少模型内存占用。 GE 提供对 PyTorch、TensorFlow 前…

作者头像 李华
网站建设 2026/9/10 10:41:27

SpringBoot健康管理系统设计与实现

1. 项目概述与核心价值这个基于SpringBoot的个人健康管理系统是我在指导计算机专业毕业设计时经常推荐的一个经典选题。它完美融合了当下企业级开发的主流技术栈和健康管理这个热门领域&#xff0c;既能展示学生的全栈开发能力&#xff0c;又具备实际应用价值。系统采用经典的M…

作者头像 李华
网站建设 2026/9/10 10:41:03

表格结构识别全流程指南:从预处理到TEDS竞赛实践

简介&#xff1a;面向文档图片表格结构识别赛题的算法竞赛源码包&#xff0c;源自同花顺算法挑战赛2022春季赛&#xff0c;适合计算机、数学、电子信息等专业学生作为课程设计、毕业设计或竞赛复现参考。资源围绕表格结构识别任务提供完整Python实现&#xff0c;包含模型训练、…

作者头像 李华
网站建设 2026/9/10 10:40:54

旧Mac如何升级最新macOS:OpenCore Legacy Patcher指南

旧Mac如何升级最新macOS&#xff1a;OpenCore Legacy Patcher指南 【免费下载链接】OpenCore-Legacy-Patcher Experience macOS just like before 项目地址: https://gitcode.com/GitHub_Trending/op/OpenCore-Legacy-Patcher 点开系统设置&#xff0c;发现macOS更新按钮…

作者头像 李华