Refine 无限滚动列表动态排序实战useInfiniteList 的 sorters 用法与源码解析【免费下载链接】refineA React Framework for building internal tools, admin panels, dashboards B2B apps with unmatched flexibility.项目地址: https://gitcode.com/GitHub_Trending/re/refine本文以 Refine 官方文档中useInfiniteList的排序Sorting实时预览示例为核心讲解如何在未知总记录数的无限加载列表中接入可动态切换的排序功能并深入其底层实现与测试依据。读完本文你将掌握useInfiniteList的sorters配置、fetchNextPage分页协作、CrudSort数据结构以及数据最终如何流入 data provider 的getList方法。为什么需要useInfiniteList在 Refine 中useInfiniteList是 TanStack Query 的useInfiniteQuery的扩展实现用于从某个resource资源中检索数据并原生支持分页pagination、排序sort和过滤filter配置。它非常适合记录总数未知、用户点击按钮逐页加载下一页的列表场景——比如商品列表、博客文章流、无限滚动面板等。它的两个核心约定出自 useInfiniteList 官方文档它使用 data provider 的getList方法作为查询函数该 data provider 通过Refine组件传入它使用**查询键query key**来缓存数据query key 由传入的属性生成可通过 TanStack Query Devtools 查看。本文聚焦的排序能力正是通过useInfiniteList的sorters属性实现的useInfiniteList会将其透传给 data provider 的getList方法动态改变sorters属性会触发一次新的请求——这就是动态排序能落地的根本原因。排序实时预览一个可直接运行的最小示例文档中的排序示例_sorting-live-preview.md以products资源为例构建了一个完整的商品列表页页面顶部有一个切换排序方向的按钮点击后按商品name字段在asc升序与desc降序之间切换同时保留无限分页能力。以下是完整代码含演示环境所需的挂载代码// visible-block-start import { useState } from react; import { useInfiniteList, HttpError } from refinedev/core; interface IProduct { id: number; name: string; material: string; } const ProductList: React.FC () { //highlight-next-line const [order, setOrder] useStateasc | desc(asc); const { result: { data, hasNextPage, hasPreviousPage }, query: { isError, isLoading, fetchNextPage, isFetchingNextPage }, } useInfiniteListIProduct, HttpError({ resource: products, //highlight-start sorters: [ { field: name, order, }, ], //highlight-end }); if (isLoading) { return pLoading/p; } if (isError) { return pSomething went wrong/p; } const allPages [].concat(...(data?.pages ?? []).map((page) page.data)); return ( div {/* highlight-start */} button onClick{() setOrder((prev) (prev asc ? desc : asc))} toggle sort /button {/* highlight-end */} ul {allPages.map((product) ( li key{product.id} {product.name} - ({product.material}) /li ))} /ul {hasNextPage ( button onClick{() fetchNextPage()} disabled{isFetchingNextPage} {isFetchingNextPage ? Loading more... : Load More} /button )} /div ); }; // visible-block-end演示环境实时预览的挂载部分展示了如何在 Refine 中注册资源并渲染该组件setInitialRoutes([/products]); setRefineProps({ resources: [ { name: products, list: /products, }, ], }); render( ReactRouter.BrowserRouter RefineHeadlessDemo ReactRouter.Routes ReactRouter.Route path/products element{ProductList /} / /ReactRouter.Routes /RefineHeadlessDemo /ReactRouter.BrowserRouter, );该代码块在文档中通过SortingLivePreview /组件被引入 index.md作为Sorting章节的核心演示。逐段拆解动态排序是如何实现的这个示例虽短却完整覆盖了 Refine 无限列表的五个关键机制值得逐段拆解1. 用useState持有排序方向const [order, setOrder] useStateasc | desc(asc);order的取值被限定为asc | desc字面量联合类型这与 Refine 的CrudSort类型约束一致保证传入sorters的值永远合法。2. 把sorters交给useInfiniteListsorters: [ { field: name, order, }, ],sorters是一个数组每个元素是一个CrudSort对象由field排序字段与order排序方向组成。示例中按name字段排序order直接取组件状态——因此修改状态即修改查询参数。3. 点击按钮切换排序方向onClick{() setOrder((prev) (prev asc ? desc : asc))}切换order后sorters引用发生变化useInfiniteList检测到查询参数变更自动触发新请求。此时由于 query key 随之改变TanStack Query 会发起一次全新查询而不是复用旧缓存。4. 合并所有页的数据用于渲染const allPages [].concat(...(data?.pages ?? []).map((page) page.data));这是 TanStack Query 无限查询的标准写法data.pages是累积的所有页数据page.data是getList返回的当页记录数组。合并后即可一次性渲染全部已加载记录。5. 分页与加载状态控制{hasNextPage ( button onClick{() fetchNextPage()} disabled{isFetchingNextPage} {isFetchingNextPage ? Loading more... : Load More} /button )}hasNextPage是否还有下一页fetchNextPage()请求下一页数据isFetchingNextPage下一页加载中标志用于禁用按钮并给出 UI 反馈此外isLoading/isError负责首屏加载与错误态hasPreviousPage表示是否存在上一页配合fetchPreviousPage使用。注意示例从解构对象中同时取出了result与query两个命名空间——result包含无限查询的数据结果data、hasNextPage等query包含查询状态与方法isError、isLoading、fetchNextPage、isFetchingNextPage。这是 Refine v5 中useInfiniteList返回值的标准结构。从源码看sorters的流转链路sorters并非useInfiniteList特有而是 Refine 数据 hooks 的通用查询参数。在 useInfiniteList 源码 中可以看到它的类型定义type BaseInfiniteListProps { /** * Metadata query for dataProvider */ meta?: MetaQuery; /** * Pagination properties */ pagination?: Pagination; /** * Sorter parameters */ sorters?: CrudSort[]; /** * Filter parameters */ filters?: CrudFilter[]; /** * If there is more than one dataProvider, you should use the dataProviderName that you will use */ dataProviderName?: string; };对应 packages/core/src/hooks/data/useInfiniteList.ts 中的BaseInfiniteListProps从源码结构可以推断出以下完整链路useInfiniteList接收sorters、pagination、filters等查询参数内部借助handlePaginationParams归一化分页参数、pickDataProvider多 data provider 选择、getNextPageParam/getPreviousPageParam计算下一页/上一页游标等辅助函数准备查询上下文这些辅助函数与useInfiniteList同属 definitions/helpers 体系后者专门为无限分页逻辑提供单元测试保障最终把sorters原样传入所选 data provider 的getList方法由 data provider 决定如何把sorters转换为 API 查询参数——例如 REST 风格 provider 会将其映射为_sort/_order之类的查询串GraphQL provider 则会把它翻译成order_by参数。因此排序语义的最终解释权在 data provideruseInfiniteList只负责把sorters稳定地传递下去并在参数变化时触发新查询。这也解释了为什么同一个sorters配置在不同 data provider 下会产生不同的网络请求形态。支撑排序的分页机制pagination与total的获取排序生效的前提是分页数据能正确加载。useInfiniteList支持与useList相同的分页属性并透传给getListimport { useInfiniteList } from refinedev/core; const postListQueryResult useInfiniteList({ resource: posts, pagination: { currentPage: 3, pageSize: 8 }, });动态修改pagination属性会触发一次新请求fetchNextPage方法会将pagination.currentPage加一然后触发新请求——这正是Load More按钮背后的实现。关于总记录数rowCountgetList被useInfiniteList调用时理想情况下应返回总行数rowCount。不同 provider 的获取方式各异REST Providers通常从x-total-count响应头读取GraphQL Providers常从pageInfo.total等字段获取其他 Provider遵循各自约定。若 data provider 未返回具体计数getList可能退化为使用当前分页数据数组的长度作为rowCount。该机制的完整说明见 data provider 的 getList 文档。完整属性参考一次掌握useInfiniteListuseInfiniteList的可用属性来自 index.md 的 Properties 章节整理如下属性说明resource必填传给getList的资源名通常对应 API 端点路径存在同名资源时可用identifier区分dataProviderName指定使用哪个 data provider存在多个 provider 时filters过滤条件数组CrudFilter[]传给getListsorters排序条件数组CrudSort[]传给getList即本文核心pagination分页配置currentPage、pageSize、modeoff/client/serverqueryOptions透传给 TanStack QueryuseQuery/useInfiniteQuery的额外选项meta传给 data provider 的附加信息如自定义 headers、GraphQL 查询生成successNotification拉取成功后的自定义成功通知errorNotification拉取失败后的自定义错误通知liveMode实时更新模式auto或manual需 Live ProvideronLiveEvent订阅到新实时事件时的回调liveParams传给 liveProvidersubscribe方法的参数overtimeOptions请求超时提示interval毫秒间隔与onInterval每次触发回调几个值得展开的属性用法pagination.mode决定是否使用服务端分页useInfiniteList({ pagination: { mode: off, }, });sorters/filters的标准写法useInfiniteList({ sorters: [ { field: title, order: asc, }, ], filters: [ { field: title, operator: contains, value: Foo, }, ], });meta常用于自定义 data provider 行为。例如给getList传递额外请求头useInfiniteList({ 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, }; }, //... };overtimeOptions用于请求耗时过长时的提示const { overtime } useInfiniteList({ //... overtimeOptions: { interval: 1000, onInterval(elapsedInterval) { console.log(elapsedInterval); }, }, }); // overtime.elapsedTime 依次为 undefined, 1000, 2000, 3000 4000, ... // 可用于条件渲染 { overtime.elapsedTime 4000 divthis takes a bit longer than expected/div; }返回值结构useInfiniteList返回 TanStack QueryuseInfiniteQuery的全部返回值并额外增加overtime查询结果类型为InfiniteQueryObserverResult{ data: TData[]; total: number }, TError按页累积的data、总行数total与查询状态/方法overtime: { elapsedTime?: number }elapsedTime为已耗时毫秒请求完成后变为undefined。这也是示例代码中result/query两个命名空间的由来result承载数据结果query承载请求状态与方法。进阶cursor 分页与自定义getNextPageParam无限列表的经典问题是如何确定下一页。Refine 默认支持基于游标cursor的分页也允许你完全自定义。cursor 分页的数据准备某些 API 使用 cursor 分页游标可为数字或字符串以查询参数传给 API。你需要在 data provider 的getList中处理getList: async ({ resource, pagination }) { const { currentPage } pagination; const { data } await axios.get( https://api.fake-rest.refine.dev/${resource}?cursor${currentPage || 0}, ); return { data: data[resource], total: 0, // cursor 分页下 total 只需定义为 0 }; },然后填充下一页游标getList: async ({ resource, pagination }) { const { currentPage } pagination; const { data } await axios.get( https://api.fake-rest.refine.dev/${resource}?cursor${currentPage || 0}, ); return { data: data[resource], total: 0, cursor: { next: data.cursor.next, prev: data.cursor.prev, }, }; },Refine 默认期望你返回cursor对象但并非强制——因为有些 API 并不采用这种模式。覆盖getNextPageParam若 API 行为特殊可在queryOptions中覆盖getNextPageParam自行返回下一页游标。覆盖后你可以访问lastPage与allPagesimport { useInfiniteList } from refinedev/core; const { data, error, hasNextPage, isLoading, fetchNextPage, isFetchingNextPage, } useInfiniteList({ resource: posts, queryOptions: { getNextPageParam: (lastPage, allPages) { // 返回最后一篇 post 的 id 作为下一页游标 const { data } lastPage; const lastPost data[data.length - 1]; return lastPost.id; }, }, });测试验证排序与分页行为的单测依据仓库为useInfiniteList提供了完整的单元测试useInfiniteList.spec.tsx从测试角度印证了本文介绍的行为with rest json server在MockJSONServer下调用useInfiniteList({ resource: posts })断言data.pages长度为 1、首页data长度为 2、total为 2——验证getList响应被正确累积进pageshasNextPage is truthy设置pagination: { pageSize: 1 }后断言hasNextPage为真——验证有更多数据时允许继续加载passes updated currentPage to data provider when fetching next page通过getListMock断言调用fetchNextPage时传给 data provider 的currentPage确实递增——这正是Load More按钮的底层行为。排序场景下同理sorters是查询参数的一部分任何order或field的变化都会改变查询条件并触发新的数据请求这由 TanStack Query 的查询键机制保证sorters参与 query key 的生成。完整示例与延伸阅读仓库提供了开箱即用的完整示例项目 examples/use-infinite-list可直接运行体验无限列表的排序、过滤与分页组合效果本文示例之外同一文档目录下还有 基础用法示例 与 过滤实时预览示例后者展示了filters配合下拉框动态过滤的同类模式以及承载全部章节的 useInfiniteList 完整文档若想深入自定义 data provider可继续阅读 data provider 指南理解getList如何把sorters、filters、pagination翻译为具体的 API 请求。【免费下载链接】refineA React Framework for building internal tools, admin panels, dashboards B2B apps with unmatched flexibility.项目地址: https://gitcode.com/GitHub_Trending/re/refine创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
