ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

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

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/refineuseList是 Refine 核心包refinedev/core中最常用的数据获取 Hook它建立在 TanStack Query 的useQuery之上为列表类资源的分页、排序、过滤、实时更新与超时反馈提供了开箱即用的能力。本文以 Refine 仓库中 useList 官方文档 为主体结合 useList 源码、单元测试 与 simple-rest 数据提供器 的实现细节带你从 API 用法一路深入到查询键query key生成、客户端分页切片、通知处理与实时订阅的底层原理帮助你写出可扩展、可维护的列表页面。useList 是什么useQuery 的“列表化”扩展从定义上看useList是 TanStack QueryuseQuery的扩展版本——它完整继承了useQuery的全部能力缓存、重试、enabled、select等并在此基础上增加了 Refine 特有的列表语义查询函数query function内部调用dataProvider的getList方法作为查询函数。getList接收resource、pagination、sorters、filters、meta等参数返回{ data, total }。查询键query key由 Hook 传入的属性自动生成用于缓存数据。你可以借助 TanStack Query Devtools 直接观察到这个键的完整结构。从源码看useList的返回结构被设计为{ query, result, overtime }三部分见 useList.tsexport type UseListReturnTypeTData, TError { query: QueryObserverResultGetListResponseTData, TError; result: { data: TData[]; total: number | undefined; [key: string]: any; }; } UseLoadingOvertimeReturnType;其中query就是原生的useQuery返回值包含isLoading、isError、isFetching等状态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 } useListIProduct, HttpError({ resource: products, }); const products result.data ?? []; if (query.isLoading) { return divLoading.../div; } if (query.isError) { return divSomething went wrong!/div; } return ( ul {products.map((product) ( li key{product.id} h4 {product.name} - ({product.material}) /h4 /li ))} /ul ); };几个值得注意的细节泛型参数useListIProduct, HttpError中的第一个类型参数是查询函数返回的记录类型第二个是自定义错误类型需继承HttpError。它们与 TanStack Query 的TQueryFnData、TError一一对应。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.tsmode默认为servercurrentPage默认为1pageSize默认为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, }; }也就是说客户端分页是借助useQuery的select机制实现的切片逻辑发生在查询结果被消费之前。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 状态驱动currentPage与pageSize改动任一状态都会触发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 } useListIProduct, HttpError({ resource: products, pagination: { currentPage, pageSize, }, }); const products result.data ?? []; if (query.isLoading) { return divLoading.../div; } if (query.isError) { return divSomething 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因为服务端不需要按页码请求改动分页不应产生新的网络请求。排序Sortingsorters 属性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] useStateasc | desc(asc); const { result, query } useListIProduct, HttpError({ resource: products, sorters: [ { field: name, order, }, ], }); const products result.data ?? []; if (query.isLoading) { return divLoading.../div; } if (query.isError) { return divSomething 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 接口定义。过滤Filteringfilters 属性useList通过filters属性支持过滤并原样透传给getList。动态修改filters会触发新的请求。其类型为CrudFilter[]每个过滤条件形如{ field, operator, value }useList({ filters: [ { field: title, operator: contains, value: Foo, }, ], });来自 _filtering-live-preview.md 的示例演示了通过下拉框切换过滤值material等于Cotton/Bronze/Plasticimport { 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 } useListIProduct, HttpError({ resource: products, filters: [ { field: material, operator: eq, value, }, ], }); const products result.data ?? []; if (query.isLoading) { return divLoading.../div; } if (query.isError) { return divSomething 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支持eq、ne、contains、gt、gte、lt、lte、between、in、and、or等多种语义具体字段定义参见 CrudFilters 接口。在 simple-rest 提供器中filters由generateFilter转换为 URL 查询参数后与分页、排序参数合并见 provider.ts。实时更新Realtime Updates订阅与三种 live 属性该功能仅在配置了 Live Provider 时可用。当useList挂载时它内部会调用liveProvider的subscribe方法传入channel、resource等参数用于订阅实时事件。从源码看useList.ts订阅通过useResourceSubscription完成channel为resources/${resource?.name}订阅事件类型为*全部事件订阅参数包含meta、pagination、hasPagination、sorters、filters以及标识为subscriptionType: useList用户传入的liveParams也会被合并进去。文档中还提到了一个值得注意的派生关系useTable、useSelect、useInfiniteList等 Hook 内部都基于useList实现因此它们会订阅相同的事件通道参见 live-provider 文档 中“派生 Hook 订阅同一事件”的说明。useList提供以下与实时相关的属性属性说明示例liveMode收到相关实时事件后是自动更新数据auto还是手动处理manualliveMode: autoonLiveEvent收到订阅事件时的回调函数onLiveEvent: (event) console.log(event)liveParams透传给liveProvider.subscribe方法的额外参数可自定义订阅所需的上下文useList({ liveMode: auto, onLiveEvent: (event) { console.log(event); }, });属性详解从 resource 到 overtimeOptionsresource必填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且该名称会进入查询键保证不同提供器的缓存相互隔离。queryOptionsqueryOptions用于把额外选项透传给 TanStack Query 的useQuery例如重试次数、enabled、select、staleTime等useList({ queryOptions: { retry: 3, }, });注意两点实现细节enabled的默认逻辑当用户未显式设置enabled时Hook 会以!!resource?.name作为默认值即资源名缺失时查询自动禁用useList.ts。select的合并顺序用户传入的select会在客户端分页切片之后执行因此select接收到的已是切片后的数据useList.ts。源码注释同时提醒如果select未被useCallback记忆化它会在每次渲染时重新执行。metameta是 Refine 中用于向数据提供器传递额外信息的特殊属性常见用途有两种针对特定场景定制数据提供器的行为用纯 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失败时先通过useOnError的checkError触发鉴权错误处理再以key: ${identifier}-useList-notification作为通知标识调用handleNotification并用translate生成默认错误文案。overtimeOptions当请求耗时过长时可以用overtimeOptions开启加载超时反馈便于展示“请求比预期更久”的提示。其中interval是回调触发的时间间隔毫秒onInterval是每个间隔触发的回调。Hook 返回的overtime对象中的elapsedTime表示已耗时毫秒请求完成时变为undefinedconst { overtime } useList({ //... overtimeOptions: { interval: 1000, onInterval(elapsedInterval) { console.log(elapsedInterval); }, }, }); console.log(overtime.elapsedTime); // undefined, 1000, 2000, 3000 4000, ... // 使用示例 { elapsedTime 4000 divthis takes a bit longer than expected/div; }实现上overtime由useLoadingOvertimeHook 基于queryResponse.isFetching驱动useList.ts。返回值Return ValuesuseList返回 TanStack QueryuseQuery的全部返回值外加两个扩展字段字段类型说明queryQueryObserverResult{ data: TData[]; total: number }, TError原生useQuery返回值含isLoading、isError、isFetching、refetch等result{ data: TData[]; total: number; [key: string]: any }便捷结构data为记录数组空时为空数组total为总数overtime{ elapsedTime?: number }请求已耗时毫秒完成时为undefinedconst { overtime } useList(); console.log(overtime.elapsedTime); // undefined, 1000, 2000, 3000 4000, ...值得注意的是result通过展开queryResponse.data并覆盖data/total构建useList.ts因此在getList返回了其他字段时result也会一并透出保持了灵活性。类型参数Type ParametersuseList支持三个泛型参数用于保障类型安全类型参数说明默认值TQueryFnData查询函数返回的记录类型需继承BaseRecordBaseRecordTError自定义错误类型需继承HttpErrorHttpErrorTDataselect处理后返回的记录类型同样继承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}通道useTable、useSelect、useInfiniteList等基于它实现会继承相同的实时行为。缓存键结构化查询键为[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),仅供参考
返回列表