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] = useState<"asc" | "desc">("asc"); const { result: { data, hasNextPage, hasPreviousPage }, query: { isError, isLoading, fetchNextPage, isFetchingNextPage }, } = useInfiniteList<IProduct, HttpError>({ resource: "products", //highlight-start sorters: [ { field: "name", order, }, ], //highlight-end }); if (isLoading) { return <p>Loading</p>; } if (isError) { return <p>Something 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] = useState<"asc" | "desc">("asc");order的取值被限定为"asc" | "desc"字面量联合类型,这与 Refine 的CrudSort类型约束一致,保证传入sorters的值永远合法。
2. 把sorters交给useInfiniteList
sorters: [ { 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 provider:useInfiniteList只负责把sorters稳定地传递下去,并在参数变化时触发新查询。这也解释了为什么同一个sorters配置在不同 data provider 下会产生不同的网络请求形态。
支撑排序的分页机制:pagination与total的获取
排序生效的前提是分页数据能正确加载。useInfiniteList支持与useList相同的分页属性,并透传给getList:
import { useInfiniteList } from "@refinedev/core"; const postListQueryResult = useInfiniteList({ resource: "posts", pagination: { currentPage: 3, pageSize: 8 }, });- 动态修改
pagination属性会触发一次新请求; fetchNextPage方法会将pagination.currentPage加一,然后触发新请求——这正是"Load More"按钮背后的实现。
关于总记录数(rowCount)
getList被useInfiniteList调用时,理想情况下应返回总行数rowCount。不同 provider 的获取方式各异:
- REST Providers:通常从
x-total-count响应头读取; - GraphQL Providers:常从
pageInfo.total等字段获取; - 其他 Provider:遵循各自约定。
若 data provider 未返回具体计数,getList可能退化为使用当前分页数据数组的长度作为rowCount。该机制的完整说明见 data provider 的 getList 文档。
完整属性参考:一次掌握useInfiniteList
useInfiniteList的可用属性(来自 index.md 的 Properties 章节)整理如下:
| 属性 | 说明 |
|---|---|
resource(必填) | 传给getList的资源名,通常对应 API 端点路径;存在同名资源时可用identifier区分 |
dataProviderName | 指定使用哪个 data provider(存在多个 provider 时) |
filters | 过滤条件数组(CrudFilter[]),传给getList |
sorters | 排序条件数组(CrudSort[]),传给getList,即本文核心 |
pagination | 分页配置:currentPage、pageSize、mode("off"/"client"/"server") |
queryOptions | 透传给 TanStack QueryuseQuery/useInfiniteQuery的额外选项 |
meta | 传给 data provider 的附加信息(如自定义 headers、GraphQL 查询生成) |
successNotification | 拉取成功后的自定义成功通知 |
errorNotification | 拉取失败后的自定义错误通知 |
liveMode | 实时更新模式:"auto"或"manual"(需 Live Provider) |
onLiveEvent | 订阅到新实时事件时的回调 |
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 && <div>this 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与allPages:
import { 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响应被正确累积进pages; - "hasNextPage 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),仅供参考