news 2026/9/11 12:31:15

Refine 实战:useList 排序(sorters)与动态数据列表构建指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Refine 实战:useList 排序(sorters)与动态数据列表构建指南

Refine 实战:useList 排序(sorters)与动态数据列表构建指南

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

本文以 Refine 官方文档中useList排序功能为核心,结合 useList 完整文档 与仓库源码(packages/core/src/hooks/data/useList.ts等),系统讲解sorters属性的类型定义、传递链路、查询缓存机制以及 dataProvider 端的实际消费方式。读完本文,你将能够在 Refine 应用中独立实现一个"点击切换排序字段/方向、数据自动刷新"的列表页,并理解其底层原理。

一、useList是什么

useList是 Refine 提供的核心数据获取 Hook,它是 TanStack Query 的useQuery的扩展版本,支持其全部特性并在此基础上增加了与 Refine 数据层相关的能力。当需要按照**排序(sorters)、过滤(filters)、分页(pagination)**等条件从某个resource获取列表数据时,useList就是最直接的入口(参见 useList 文档)。

它的两个关键设计:

  • 查询函数:内部使用传给<Refine>组件的dataProvidergetList方法作为查询函数;
  • 查询缓存:根据传入的 properties 生成查询键(query key),数据会被缓存,并可在 TanStack Query Devtools 中直接观察 query key 的变化。

而本文的主角——排序,就是通过useListsorters属性实现的:把sorters传给dataProvider.getList,由数据提供方(REST / GraphQL 等)将其转换为对应的排序参数。

二、排序功能演示:一个可切换排序的商品列表

Refine 文档为排序功能提供了完整的可运行示例(即 排序 Live Preview 源码)。完整代码示例如下:

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> ); };

这段示例虽然短小,但涵盖了useList排序功能的全部核心要点:

  1. sorters是一个对象数组,每个对象由field(字段名)与order(排序方向)组成;
  2. 排序方向由 React state 驱动order"asc" | "desc"联合类型,点击 "toggle sort" 按钮在升序与降序之间切换;
  3. 动态改变sorters会触发新的请求:当orderstate 变化导致sorters内容变化时,useList会自动重新请求数据——这正是 Refine 文档明确指出的行为:"Dynamically changing thesortersproperty will trigger a new request."(参见 useList 文档)。

对应的路由注册与渲染挂载代码如下:

setRefineProps({ resources: [ { name: "products", list: "/products", }, ], }); render( <ReactRouter.BrowserRouter> <RefineHeadlessDemo> <ReactRouter.Routes> <ReactRouter.Route path="/products" element={<ProductList />} /> </ReactRouter.Routes> </RefineHeadlessDemo> </ReactRouter.BrowserRouter>, );

三、sorters的类型定义:CrudSortCrudSorting

在 Refine 的核心类型中,排序相关的类型定义位于 packages/core/src/contexts/data/types.ts:

export type SortOrder = "desc" | "asc" | null; export type CrudSort = { field: string; order: "asc" | "desc"; }; export type CrudSorting = CrudSort[];

需要特别注意的是:

  • CrudSortorder字段在排序数组中仅接受"asc" | "desc"两种字面量,因此当你把 state 声明为useState<"asc" | "desc">("asc")时,类型系统就能保证sorters永远合法;
  • 类型别名CrudSorting = CrudSort[]表示整个排序条件数组,一个列表请求可以同时携带多字段排序
    sorters: [ { field: "name", order: "asc" }, { field: "createdAt", order: "desc" }, ],
  • 与过滤类似,useListsorters属性注释明确指出其会被原样传给dataProvidergetList方法:"sorterswill be passed to thegetListmethod from thedataProvideras a parameter. It is used to send sort query parameters to the API."(参见 useList 文档)。

四、源码级原理:sortersuseList内部如何流转

要理解"改变排序自动刷新数据"的机制,需要阅读 useList 实现。其内部流程可以拆解为三条链路:

4.1 查询函数:把sorters交给getList

useList在构造queryFn时,会从解构出的prefferedSorters中把排序条件完整透传给数据提供方:

queryFn: (context) => { const meta = { ...combinedMeta, ...prepareQueryContext(context), }; return getList<TQueryFnData>({ resource: resource?.name ?? "", pagination: prefferedPagination, filters: prefferedFilters, sorters: prefferedSorters, // 排序条件透传给 getList meta, }); },

由此可见,sorterspaginationfilters一样,是getList参数中的一等公民。真正的查询执行完全由你配置的 dataProvider 决定。

4.2 查询键:sorters参与 query key 生成

useList使用useKeys()构造稳定的查询键,sorters是其中的一部分:

queryKey: keys() .data(pickedDataProvider) .resource(identifier ?? "") .action("list") .params({ ...(preferredMeta || {}), filters: prefferedFilters, ...(isServerPagination && { pagination: prefferedPagination, }), ...(sorters && { sorters, }), }) .get(),

这正是"动态改变sorters会触发新请求"的根本原因:查询键是查询缓存的指纹,只要排序条件变化,query key 就变化,TanStack Query 便会视为一次新的查询并重新执行queryFn。这也是为什么文档建议可以用 TanStack Query Devtools 直接观察 query key 的构成。

4.3 实时订阅:排序参数随订阅一并下发

当使用 Live Provider 时,useList挂载后会调用liveProvidersubscribe方法(channel 形如resources/${resource?.name}),并把sorters等参数一并传入params

useResourceSubscription({ resource: identifier, types: ["*"], params: { meta: combinedMeta, pagination: prefferedPagination, hasPagination: isServerPagination, sorters: prefferedSorters, filters: prefferedFilters, subscriptionType: "useList", ...liveParams, }, channel: `resources/${resource?.name}`, ... });

这意味着实时场景下,订阅上下文同样携带当前的排序条件,相关实时事件("*"类型)可以按需触发数据更新(liveMode: "auto" | "manual")。

4.4 返回值结构

useList返回 TanStack QueryuseQuery的全部返回值,并额外整理出resultovertime

return { query: queryResponse, // QueryObserverResult result: { ...queryResponse?.data, data: queryResponse?.data?.data || EMPTY_ARRAY, // 默认空数组,避免 undefined 报错 total: queryResponse?.data?.total, }, overtime: { elapsedTime }, };

这也是示例代码中const products = result.data ?? [];query.isLoadingquery.isError并行使用的类型依据。

五、dataProvider 端:以 simple-rest 的generateSort为例

sorters到达getList后如何变成真实的 API 查询参数,取决于数据提供方。以仓库内置的@refinedev/simple-rest为例,其getList实现位于 packages/simple-rest/src/provider.ts,排序由工具函数 generateSort.ts 处理:

import type { CrudSorting } from "@refinedev/core"; export const generateSort = (sorters?: CrudSorting) => { if (sorters && sorters.length > 0) { const _sort: string[] = []; const _order: string[] = []; sorters.map((item) => { _sort.push(item.field); _order.push(item.order); }); return { _sort, _order, }; } return; };

可以看到,simple-rest 采用_sort_order两个并列参数传递排序信息:_sort收集所有field_order收集对应的order,最终形成形如?_sort=name&_order=asc的查询串。因此:

  • 多字段排序天然支持:传入多个CrudSort对象即可,_sort_order会按索引一一对应;
  • 无排序条件时返回undefinedprovider.ts中会跳过该参数的拼装,不会向 API 发送多余的排序参数。

如果你使用的是 GraphQL 数据提供方(如@refinedev/graphql@refinedev/hasura等),sorters则会被映射为 GraphQL 的order_by/sort等查询变量。这印证了文档中的结论:sorters语义统一,落地方式由 dataProvider 自行决定。关于自定义 dataProvider 中getList的完整契约,可参考 文档 data 部分 中创建数据提供方的教程。

六、与分页、过滤协同使用

排序通常与分页、过滤同时出现。文档中的分页示例展示了三者的协作模式(参见 分页 Live Preview):

const [currentPage, setCurrentPage] = useState(1); const [pageSize, setPageSize] = useState(5); const { result, query } = useList<IProduct, HttpError>({ resource: "products", pagination: { currentPage, pageSize, }, });

pagination中,currentPage(默认1)、pageSize(默认10)与mode"server" | "client" | "off",默认"server")共同控制分页行为。其中mode: "client"时,useList会在客户端对getList返回的完整数据做切片(见 useList.ts 中memoizedSelect的实现);mode: "off"则完全关闭分页。

过滤则通过filters属性传入,配合CrudFilter类型与丰富的操作符(eqcontainsstartswithbetweenor/and组合等,完整清单见 types.ts):

filters: [ { field: "title", operator: "contains", value: "Foo", }, ],

实际开发中一个典型组合如下——排序、过滤、分页三者并存,任一状态变化都会因 query key 变化而自动触发重新请求:

useList<IProduct, HttpError>({ resource: "products", sorters: [{ field: "name", order }], filters: [{ field: "material", operator: "eq", value: "steel" }], pagination: { currentPage, pageSize }, });

七、useList其余常用属性速览

除排序外,useList还提供以下常用属性(完整定义见 useList 文档 与 useList.ts):

属性作用
resource(必填)传给getList的资源标识,通常对应 API 路径;多资源同名时可改用identifier匹配
dataProviderName配置了多个 dataProvider 时,指定使用哪一个
filters过滤条件数组(CrudFilter[]),透传给getList
sorters排序条件数组(CrudSort[]),透传给getList
pagination分页配置:currentPage/pageSize/mode
queryOptions透传给底层useQuery的选项,如retryenabledselect
meta附加元数据,可用于自定义 dataProvider 行为或生成 GraphQL 查询
successNotification/errorNotification自定义成功 / 失败通知(需配置 NotificationProvider)
liveMode/onLiveEvent/liveParams实时订阅相关(需配置 LiveProvider)
overtimeOptions请求超时监测,配合返回的overtime.elapsedTime展示"加载过久"提示

其中overtimeOptions的典型用法(来自文档):

const { overtime } = useList({ resource: "products", overtimeOptions: { interval: 1000, onInterval(elapsedInterval) { console.log(elapsedInterval); }, }, }); // overtime.elapsedTime 依次为 undefined, 1000, 2000, 3000 ... { overtime.elapsedTime >= 4000 && <div>this takes a bit longer than expected</div>; }

八、小结

围绕useList的排序能力,本文可以提炼出以下关键结论:

  1. 使用sorters数组描述排序:每个元素是{ field, order },类型为CrudSort,支持多字段、可动态变更;
  2. 动态变更即自动刷新sorters参与查询键生成(见 useList.ts 的queryKey构造),变化会触发新请求,无需手动调用refetch
  3. 透传而非解释useList只负责把sorters原样交给dataProvider.getList,REST 提供方(如 simple-rest 的generateSort)与 GraphQL 提供方各自决定如何映射为查询参数;
  4. 实时能力内置:启用 Live Provider 后,排序条件随订阅参数下发,配合liveMode可实现实时更新列表;
  5. 类型安全贯穿始终SortOrderCrudSortCrudSorting定义于 packages/core/src/contexts/data/types.ts,配合useList<IProduct, HttpError>的泛型参数,可以构建出编译期即有保障的列表逻辑。

参照文档中的 Live Preview 示例(排序示例、基础用法、分页示例),并对照源码 useList.ts 与 generateSort.ts,你就可以在真实项目中快速落地一个支持动态排序、过滤与分页的数据列表页。

【免费下载链接】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/11 12:30:27

NPU/GPGPU乱序执行设计:面向AI负载的粗粒度OoO实践

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/11 12:29:50

MODBUS从帧格式到CRC校验:嵌入式调试实战完整梳理

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/11 12:26:45

RK3568+OpenHarmony多路显示全栈移植实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/11 12:24:22

RP2040 PIO本质:硬件状态机编程与确定性时序实现

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/11 12:22:12

AI Agent记忆机制:从上下文窗口到Redis向量检索的落地实践

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华