news 2026/9/23 4:45:37

Relay 17 连接(Connection)重取与筛选:使用 usePaginationFragment 的 refetch 实现过滤与排序

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Relay 17 连接(Connection)重取与筛选:使用 usePaginationFragment 的 refetch 实现过滤与排序

Relay 17 连接(Connection)重取与筛选:使用 usePaginationFragment 的 refetch 实现过滤与排序

导读:本指南聚焦 Relay 中“带筛选条件的连接数据重取(refetching connections)”这一核心场景。当你的列表需要根据搜索词、排序方式等参数改变查询结果时,如何既能保留分页能力、又能在参数变化时重新获取数据?本文以usePaginationFragmentrefetch函数为主线,讲解筛选参数的传递、分页时参数的保留机制、refetch 的触发时机与变量合并规则,并结合react-relayrelay-runtime的源码(usePaginationFragment.js、useRefetchableFragmentInternal.js)揭示底层实现。读完你将掌握在 Relay 17 中实现搜索型列表、排序切换等实战方案。


在 GraphQL 中,连接(Connection)字段往往不只是返回一个固定列表,而是可以接收参数,用来筛选(filter)结果集或改变排序方式(sort)。典型场景包括:

  • 搜索 typeahead(自动补全):结果列表由用户输入的搜索词动态过滤;
  • 切换评论排序模式:对某篇文章的评论,用户可选择不同的排序方式,服务器返回完全不同的评论集合;
  • 改变信息流(News Feed)的排序方式

本文要解决的问题正是:当这些“筛选参数”变化时,如何让已渲染的连接数据重新获取(refetch),同时不影响已有的分页(pagination)能力。

本文基于仓库中 refetching-connections.md 编写,并结合当前仓库中 Relay 17 版本的源码进行深入验证。


一、连接字段如何接收筛选参数

在 GraphQL 层面,连接字段可以接收任意参数来筛选或排序结果。例如,下面的 fragment 请求了一个按order_by排序、按search_term过滤的好友列表:

fragment UserFragment on User { name friends(order_by: DATE_ADDED, search_term: "Alice", first: 10) { edges { node { name age } } } }

其中order_bysearch_term筛选/排序参数,而first分页参数(表示取前 10 条)。firstafterbeforelast这类分页参数由 Relay 自动管理,但筛选参数则由业务逻辑控制。

在 Relay 中,我们通常不把筛选参数写死成字面量,而是通过 GraphQL 变量(variables)传入,这样运行时才能动态改变它们。使用usePaginationFragment时,fragment 中可以这样写:

type Props = { userRef: FriendsListComponent_user$key, }; function FriendsListComponent(props: Props) { const userRef = props.userRef; const {data, ...} = usePaginationFragment( graphql` fragment FriendsListComponent_user on User { name friends( order_by: $orderBy, search_term: $searchTerm, after: $cursor, first: $count, ) @connection(key: "FriendsListComponent_user_friends_connection") { edges { node { name age } } } } `, userRef, ); return (...); }

这里有几个关键点:

  • $orderBy$searchTerm是筛选变量,$cursor$count是分页变量;
  • @connection(key: "FriendsListComponent_user_friends_connection")指令告诉 Relay 这是一个需要规范化的连接字段,key用于在 Relay store 中唯一标识该连接(详见 transform_connections.rs 中build_connection_metadata等实现);
  • 使用usePaginationFragment的前提是 fragment 同时被标注@refetchable(queryName: "..."),编译器会为该 fragment 自动生成一个分页查询(可参考 pagination.md 中的完整写法)。

二、分页时筛选参数会被保留

usePaginationFragment返回的loadNext(count)用于加载下一页。一个容易忽视但至关重要的行为是:调用loadNext时,会继续使用初始查询时的筛选参数值

type Props = { userRef: FriendsListComponent_user$key, }; function FriendsListComponent(props: Props) { const {data, loadNext} = usePaginationFragment( graphql` fragment FriendsListComponent_user on User { name friends(order_by: $orderBy, search_term: $searchTerm) @connection(key: "FriendsListComponent_user_friends_connection") { edges { node { name age } } } } `, userRef, ); return ( <> <h1>Friends of {data.name}:</h1> <List items={data.friends?.nodes}>{...}</List> {/* Loading the next items will use the original order_by and search_term values used for the initial query */} <Button onClick={() => loadNext(10)}>Load more friends</Button> </> ); }

注意:调用loadNext会沿用初始查询时的order_bysearch_term值。在分页过程中,这些值不会(也不应该)改变——因为后续页面必须与第一页处于同一筛选条件下,分页才有意义。

从源码看,这一行为由 useLoadMoreFunction.js 中的loadMore回调实现:它从 fragment selector 中取出parentVariablesfragmentVariables合并为baseVariables,再调用getPaginationVariables生成分页请求的变量:

const parentVariables = fragmentSelector.owner.variables; const fragmentVariables = fragmentSelector.variables; const extraVariables = options?.UNSTABLE_extraVariables; const baseVariables = { ...parentVariables, ...fragmentVariables, }; const paginationVariables = getPaginationVariables( direction, count, cursor, baseVariables, {...extraVariables}, paginationMetadata, );

而 getPaginationVariables.js 只会覆盖cursorcount这两个分页变量,其余变量(包括orderBysearchTerm)原样保留在baseVariables中,从而实现“分页沿用原筛选条件”的效果。同时它会将反向分页的游标/数量变量置为null,确保 forward/backward 两种方向互不干扰:

const paginationVariables = { ...baseVariables, ...extraVariables, [forwardMetadata.cursor]: cursor, [forwardMetadata.count]: count, }; if (backwardMetadata && backwardMetadata.cursor) { paginationVariables[backwardMetadata.cursor] = null; } if (backwardMetadata && backwardMetadata.count) { paginationVariables[backwardMetadata.count] = null; }

需要临时在分页请求上附加额外变量时,可以使用loadNext(count, {UNSTABLE_extraVariables: {...}}),但注意不能cursorcount放进extraVariables——源码中对此有显式warning(见 getPaginationVariables.js)。


三、用 refetch 以不同的参数重新获取连接

如果我们需要用不同的变量重新获取连接——比如用户改变了搜索词或排序方式——就不能再用loadNext,而应使用usePaginationFragment提供的refetch函数。其用法与 Refetching Fragments with Different Data 一节中介绍的useRefetchableFragmentrefetch完全一致。

3.1 完整示例:搜索词变化时自动重取

/** * FriendsListComponent.react.js */ import type {FriendsListComponent_user$key} from 'FriendsListComponent_user.graphql'; const React = require('React'); const {useState, useEffect} = require('React'); const {graphql, usePaginationFragment} = require('react-relay'); type Props = { searchTerm?: string, user: FriendsListComponent_user$key, }; function FriendsListComponent(props: Props) { const searchTerm = props.searchTerm; const {data, loadNext, refetch} = usePaginationFragment( graphql` fragment FriendsListComponent_user on User { name friends( order_by: $orderBy, search_term: $searchTerm, after: $cursor, first: $count, ) @connection(key: "FriendsListComponent_user_friends_connection") { edges { node { name age } } } } `, props.user, ); useEffect(() => { // When the searchTerm provided via props changes, refetch the connection // with the new searchTerm refetch({first: 10, search_term: searchTerm}, {fetchPolicy: 'store-or-network'}); }, [searchTerm]) return ( <> <h1>Friends of {data.name}:</h1> {/* When the button is clicked, refetch the connection but sorted differently */} <Button onClick={() => refetch({first: 10, orderBy: 'DATE_ADDED'}); }> Sort by date added </Button> <List items={data.friends?.nodes}>...</List> <Button onClick={() => loadNext(10)}>Load more friends</Button> </> ); }

3.2 提炼关键行为

逐条解读上面这个例子:

  1. refetch接受新变量集合并重新抓取 fragment。你传入的变量是生成的查询所期望变量的一个子集:生成的查询需要id(当 fragment 类型带有id字段时),以及 fragment 中传递引用的所有其他变量。在本例中,我们必须传first(要抓取的条数),并可传入不同的筛选值如orderBysearchTerm。注意这里refetch({first: 10, order_by: searchTerm})的写法来自文档示例;实际项目中变量名以你在 fragment 中声明的$orderBy/$searchTerm为准。
  2. 可能触发 Suspenserefetch会重新渲染组件,如果需要发起并等待网络请求,组件可能进入 suspend 状态(详见 Loading States with Suspense)。因此必须确保组件上层有Suspense边界来展示 fallback。
  3. refetch 是“从零开始”:调用refetch在概念上等于从头重新抓取连接,并重置分页状态。例如用不同的search_term抓取后,之前search_term的分页信息不再有意义——我们实际上是在对一个全新列表进行分页。

3.3 fetchPolicy 的选择

示例中使用了fetchPolicy: 'store-or-network'。Relay 17 中合法的fetchPolicy取值在 RelayRuntimeTypes.js 中定义为:

fetchPolicy行为
'store-or-network'若 store 中已有满足条件的数据则直接复用(跳过网络请求),否则发起网络请求
'store-and-network'先读 store 数据立即渲染,同时发起网络请求,请求完成后用最新数据更新
'network-only'始终发起网络请求,不读缓存
'store-only'只读 store,绝不发起网络请求
  • 若希望避免 Suspense:可先用fetchQuery把数据写入 store,再以fetchPolicy: 'store-only'调用refetch,此时数据已缓存、不会 suspend(完整做法见 refetching-fragments-with-different-data.md 的 "If you need to avoid Suspense" 小节)。
  • refetch还支持UNSTABLE_renderPolicy: 'full' | 'partial'(对应 RelayRuntimeTypes.js 的RenderPolicy)与onComplete回调,类型定义见 useRefetchableFragmentInternal.js。

四、源码视角:refetch 与分页是如何协同的

usePaginationFragment本质上是useRefetchableFragmentInternal(负责refetch)与useLoadMoreFunction(负责loadNext/loadPrevious)的组合。查看 usePaginationFragment.js 可以看到:

  1. 通过getPaginationMetadata从 fragment 中解析连接路径与分页元数据;
  2. 通过useRefetchableFragmentInternal获得refetch能力;
  3. 分别构造 forward / backward 两个方向的loadNext/loadPrevious
  4. 关键点refetchPagination在真正调用refetch之前,会先disposeFetchNext()disposeFetchPrevious()——即取消所有在途的分页请求
const refetchPagination = useCallback( (variables: TVariables, options: void | Options) => { disposeFetchNext(); disposeFetchPrevious(); return refetch(variables, {...options, __environment: undefined}); }, [disposeFetchNext, disposeFetchPrevious, refetch], );

这从源码层面印证了文档中的结论:refetch 会重置分页状态——旧的分页请求被主动作废,连接从第一条开始重新获取。

再看 useRefetchableFragmentInternal.js 中refetch的实现,变量合并顺序为:

const refetchVariables: VariablesOf<TQuery> = { ...(parentVariables as $FlowFixMe), ...fragmentVariables, ...providedRefetchVariables, };

即:未在refetch调用中提供的变量,会从原 fragment owner 的变量中继承。因此你不需要每次把所有变量都传齐,只需要传“发生变化的”以及必要的first(count)即可。同时,如果生成的查询需要标识符(如id)且未显式传入,Relay 会从 fragment data 中读取并自动填充:

if ( identifierInfo != null && !providedRefetchVariables.hasOwnProperty( identifierInfo.identifierQueryVariableName, ) ) { refetchVariables[identifierInfo.identifierQueryVariableName] = identifierValue; }

相关运行时期类型与getRefetchMetadatagetPaginationMetadata的返回结构可参考 getPaginationMetadata.js 与relay-runtime的 index.d.ts。@connection指令在编译器侧的处理(连接元数据构建、handle field 生成)位于 transform_connections.rs。


五、实践清单与常见陷阱

综合文档与源码,给出可复制的实践要点:

  1. fragment 需要@refetchableusePaginationFragment要求 fragment 可 refetch(作用于ViewerQuery、实现Node的类型或@fetchable类型),并配合@connection(key: "...")标注连接字段。
  2. 筛选变量通过 GraphQL 变量传入:不要把筛选条件写死成字面量,否则无法在运行时改变它们。
  3. 分页沿用原筛选条件loadNext/loadPrevious会复用初始查询的筛选变量,不要在分页时试图改变筛选条件。
  4. 改变筛选条件用refetch:传入新变量(必要时含first),Relay 会自动继承未变化的变量、填充id,并取消在途分页请求、重置分页游标
  5. 务必有Suspense边界refetch可能触发 suspend,组件上层需要Suspense包裹;想避免 Suspense 时使用fetchQuery+fetchPolicy: 'store-only'的组合。
  6. 变量命名一致性:GraphQL 变量名($orderBy/$searchTerm/$cursor/$count)必须在 fragment 中声明并在查询根(@refetchable生成查询)中可用;生成的查询会要求id(若类型有id字段)以及 fragment 中传递引用的所有其他变量。

延伸阅读

  • 分页入门与loadNext/hasNext用法:pagination.md
  • 连接(Connection)概念与游标分页:connections.md
  • 通用 fragment 重取(useRefetchableFragment):refetching-fragments-with-different-data.md
  • 高级分页(自定义 append/prepend 行为):advanced-pagination.md
  • 运行时实现:usePaginationFragment.js、useRefetchableFragmentInternal.js、getPaginationVariables.js
  • 前端
  • 开发工具

【免费下载链接】relay

Relay is a JavaScript framework for building>项目地址:https://gitcode.com/gh_mirrors/relay29/relay

点击查看免费下载

相关推荐

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

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

研发增长:从各环节改善,提高产品市场竞争力

研发增长&#xff1a;从各环节改善&#xff0c;提高产品市场竞争力——专知智库研发增长系列引言&#xff1a;研发增长的最终检验标准&#xff0c;是产品竞争力研发增长&#xff0c;不是账面上的数字游戏。它的最终检验标准只有一个&#xff1a;产品在市场上&#xff0c;能不能…

作者头像 李华
网站建设 2026/9/23 4:43:46

排气系统声学设计实战:从噪声原理到消声结构与NVH调校

一台车在你心里的“性格”&#xff0c;一半是动力给的&#xff0c;另一半其实是声音给的。排气声浪厚不厚、有没有破音、急加速时是沉闷有力还是干瘪嘶吼&#xff0c;这些都不是玄学&#xff0c;而是排气系统声学设计的结果。我做了几十个项目的排气噪声优化&#xff0c;从乘用…

作者头像 李华
网站建设 2026/9/23 4:36:03

Java递归中Scanner资源管理的最佳实践

1. 问题背景与核心痛点在Java开发中&#xff0c;递归算法与Scanner资源管理的结合使用一直是个容易被忽视的细节问题。很多开发者都遇到过这样的场景&#xff1a;在递归方法中读取用户输入时&#xff0c;程序运行后出现java.util.NoSuchElementException异常&#xff0c;或者发…

作者头像 李华
网站建设 2026/9/23 4:35:40

提示词做减法:GPT-6与Skills分工的实战指南

最近OpenAI官方关于GPT-6与Skills方向放出的指导&#xff0c;核心观点就一句话&#xff1a;提示词该做减法了。这对过去两年习惯了“长提示词等于高质量”的人来说&#xff0c;几乎是方向性急转弯。我在GPT-6上做了几轮实测&#xff0c;又把自己手上十几个项目的提示词逐条拆开…

作者头像 李华