本篇技术指南围绕 Relay 中“以不同数据重取 Fragment(Refetching Fragments with Different Data)”这一核心场景展开,讲解如何使用useRefetchableFragmentHook 与@refetchable指令,让 Fragment 在保留查询上下文的前提下,用一组全新的变量重新取数并渲染,从而支持“切换当前选中项”“渲染不同的列表内容”等交互需求。读完本文,你将掌握@refetchable的适用条件、refetch函数的变量与选项语义、以及需要避开 Suspense 时的fetchQuery替代方案,并能理解 Relay 编译器如何自动生成 refetch 查询的底层机制。
理解“以不同数据重取 Fragment”
在 Relay 中,所谓refetch a fragment(重取 Fragment),指的是拉取一份与该 Fragment 最初渲染时不同的数据。典型场景包括:
- 改变当前选中的条目(如切换商品、切换评论);
- 渲染与当前显示不同的列表内容;
- 更一般地,把当前已渲染的内容过渡到新的或不同的内容。
概念上,这意味着把当前已渲染的 Fragment重新放到一个新的查询根(query root)下,用不同的变量再取一次、再渲染一次。这里有一个关键前提:
Fragment 无法独立取数,它必须依附于某个查询(query)。因此我们无法单独“fetch 一个 fragment”,而必须借助自动生成的查询来承载重取。
这正是useRefetchableFragment存在的意义:配合@refetchable指令,Relay 编译器会自动生成一个用于重取该 Fragment 的查询,Hook 返回的refetch函数则负责以新变量执行这个查询,并让组件用最新数据重新渲染。
使用useRefetchableFragment重取 Fragment
基础示例:切换评论正文的语言
先看一个完整可运行的例子:一个展示评论正文的CommentBody组件,点击按钮即可把正文翻译成西班牙语重新取数。
import type {CommentBodyRefetchQuery} from 'CommentBodyRefetchQuery.graphql'; import type {CommentBody_comment$key} from 'CommentBody_comment.graphql'; type Props = { comment: CommentBody_comment$key, }; function CommentBody(props: Props) { const [data, refetch] = useRefetchableFragment<CommentBodyRefetchQuery, _>( graphql` fragment CommentBody_comment on Comment # @refetchable 让 Relay 为该 fragment 自动生成查询 @refetchable(queryName: "CommentBodyRefetchQuery") { body(lang: $lang) { text } } `, props.comment, ); const refetchTranslation = () => { // 传入新变量调用 refetch, // 它会以新变量重取 @refetchable 查询, // 并用最新取回的数据更新当前组件。 refetch({lang: 'SPANISH'}); }; return ( <> <p>{data.body?.text}</p> <Button onClick={() => refetchTranslation()}> Translate Comment </Button> </> ); }逐步拆解这个示例
useRefetchableFragment的用法与useFragment类似(参见 Fragments 一节),但有几点差异:- 它要求传入的 Fragment必须标注
@refetchable指令。注意@refetchable只能加在“可重取”的 Fragment 上,即声明在Viewer、Query类型上,或声明在任何实现了Node接口(即拥有id字段)的类型上,以及@fetchable类型上。 - 它返回一个
refetch函数,该函数已被 Flow 类型化,其参数类型正好是生成的查询所期望的变量集合。 - 它接受两个 Flow 类型参数:第一个是自动生成查询的类型(本例为
CommentBodyRefetchQuery),第二个通常可以被自动推断,只需传下划线_。
- 它要求传入的 Fragment必须标注
- 调用
refetch需要两类输入:- 第一个参数是新变量集合。传入一组新变量会让 Fragment 以这组新变量重新取数。你只需提供
@refetchable查询所需变量的一个子集:如果 Fragment 所在类型有id字段,查询会要求一个id;其余变量则是 Fragment 内被传递引用的那些变量。本例中我们传入了当前评论的id和新的lang变量值,以取回翻译后的正文。 - 第二个是可选的 options 参数,本例未传,因此使用默认的
fetchPolicy('store-or-network')——如果该 Fragment 的新数据已缓存,则跳过网络请求(参见 Reusing Cached Data For Render 相关章节)。
- 第一个参数是新变量集合。传入一组新变量会让 Fragment 以这组新变量重新取数。你只需提供
- 调用
refetch会触发组件重新渲染,并可能使useRefetchableFragment进入 Suspense(参见 Loading States with Suspense)。因此必须保证该组件上方有一个Suspense边界,以便在取数期间展示 fallback 加载态。
:::info 同样的行为也适用于usePaginationFragment返回的refetch函数(参见 usePaginationFragment API)。 :::
refetch的变量与选项语义
结合 useRefetchableFragment API 参考,refetch的完整签名与语义如下:
variables(对象):用于重取@refetchable查询的新变量值,需与 Fragment 内引用的 GraphQL 变量一致。但有两个重要的放宽规则:- 只需要提供打算改变的变量;Fragment 引用但被省略的变量,会回退到原始父查询中的值。因此,若想以与最初完全相同的变量重取 Fragment,直接调用
refetch({})即可。 - 对于
$id变量,除非你想用不同的id重取,否则传id是可选的——Relay 已经知道当前渲染对象的 id。其refetch返回一个disposable对象,调用disposable.dispose()可取消本次重取请求。
- 只需要提供打算改变的变量;Fragment 引用但被省略的变量,会回退到原始父查询中的值。因此,若想以与最初完全相同的变量重取 Fragment,直接调用
options(可选对象):fetchPolicy:决定是否使用缓存数据、以及在缓存可用时何时发起网络请求(完整规范见 Fetch Policies)。onComplete:每当重取请求完成时(包括任何增量数据载荷)都会被调用的回调。
useRefetchableFragment还具备与useFragment相同的订阅语义:组件自动订阅 Fragment 数据更新,如果该对象的数据在应用其他位置被更新(如新取数或 mutation),组件会自动用最新数据重新渲染;当 Fragment 的某些数据缺失且正在被父查询获取时,组件会挂起(suspend)。
与旧版RefetchContainer的差异
从useRefetchableFragment的实现历史与 API 设计看,它与 class 时代的RefetchContainer相比主要有三点变化:
- 无需再手写 refetch 查询,Relay 通过
@refetchableFragment 自动生成; - 不再区分语义模糊的
refetchVariables与renderVariables,重取总是以你提供的变量正确地取数并渲染(省略的变量回退到父查询原值); - 重取总是明确地更新组件,而
RefetchContainer时期取决于 refetch 查询内容与 Fragment 定义的对象类型,更新不一定发生。
需要避开 Suspense 时:fetchQuery+ 手动加载态
有些场景下,你不希望出现会隐藏已渲染内容的 Suspense fallback。此时可以用fetchQuery先把数据写入本地 Store,再手动维护一个加载状态:
import type {CommentBodyRefetchQuery} from 'CommentBodyRefetchQuery.graphql'; import type {CommentBody_comment$key} from 'CommentBody_comment.graphql'; type Props = { comment: CommentBody_comment$key, }; function CommentBody(props: Props) { const [data, refetch] = useRefetchableFragment<CommentBodyRefetchQuery, _>( graphql` fragment CommentBody_comment on Comment # @refetchable 让 Relay 为该 fragment 自动生成查询 @refetchable(queryName: "CommentBodyRefetchQuery") { body(lang: $lang) { text } } `, props.comment, ); const [isRefetching, setIsRefreshing] = useState(false) const refetchTranslation = () => { if (isRefetching) { return; } setIsRefreshing(true); // fetchQuery 会先取回查询并把数据写入 Relay store, // 从而保证当我们重新渲染时数据已在缓存中、不会挂起。 fetchQuery(environment, AppQuery, variables) .subscribe({ complete: () => { setIsRefreshing(false); // 查询取回 *之后*,再次调用 refetch 用最新数据渲染。 // 此时该查询的数据已缓存,因此用 'store-only' 的 // fetchPolicy 来避免挂起。 refetch({lang: 'SPANISH'}, {fetchPolicy: 'store-only'}); } error: () => { setIsRefreshing(false); } }); }; return ( <> <p>{data.body?.text}</p> <Button disabled={isRefetching} onClick={() => refetchTranslation()}> Translate Comment {isRefetching ? <LoadingSpinner /> : null} </Button> </> ); }这个方案的关键点
- 重取时我们自己维护
isRefetching状态(因为避开了挂起),用它渲染忙碌指示器或类似加载 UI,而不会隐藏已有内容; - 事件处理器中先调用
fetchQuery,它会取回查询并把数据写入本地 Relay Store;网络请求完成后,再调用refetch用更新后的数据渲染,与前面的示例类似; - 此时调用
refetch时,该 Fragment 的数据应当已经缓存在本地 Store 中,因此使用fetchPolicy为'store-only',只读取已缓存数据,避免挂起。
需要说明的是,这一“避开 Suspense”的做法是当前版本的权宜方案。原文档的附注(OssAvoidSuspenseNote)指出:在未来支持并发渲染的 React 版本中,React 将提供相应选项,避免挂起时用 Suspense fallback 隐藏已渲染内容。
深入底层:refetch是如何工作的
为了把前面的实操与 Relay 的实现对上号,这里结合本仓库源码梳理refetch的调用链(以下文件均在 packages/react-relay 下):
- useRefetchableFragment.js 是公开入口。它通过
getFragment拿到 fragment node,然后委托给内部实现useRefetchableFragmentInternal,返回[fragmentData, refetch]元组。 - useRefetchableFragmentInternal.js 是核心实现,几个值得注意的细节印证了文档语义:
- 调用
refetch时,源码把parentVariables、fragmentVariables与你传入的providedRefetchVariables合并,得到最终的重取变量集合——这正是“省略的变量回退到父查询原值”的实现依据; - 当 Fragment 有标识字段(
id等)而你未显式传入时,源码会从当前 Fragment 数据中读取identifierValue自动补上,这就是“传id是可选的”的实现依据; - 重取请求通过
createOperationDescriptor创建(强制force: true),再经loadQuery启动网络请求(必要时),随后在渲染阶段通过QueryResource.prepare读取/等待结果,并在重取查询仍在途时挂起——对应“调用 refetch 可能让组件 suspend”的行为; refetch返回{dispose: disposeQuery},即文档中的 disposable,可用于取消本次重取。
- 调用
- 在
__DEV__模式下,实现还带有一组校验函数(checkSameIDAfterRefetch、checkSameTypeAfterRefetch),会在重取后检查返回对象的id与__typename是否与 Store 中一致,若不一致会给出 warning——这保证了以不同数据重取时数据一致性问题的可诊断性。
编译器侧:@refetchable如何生成查询
@refetchable的编译端实现在 compiler/crates/relay-transforms/src/refetchable_fragment/ 目录。从 refetchable_directive.rs 的定义可以看到,该指令的完整 schema 为:
directive @refetchable( queryName: String! directives: [String!] preferFetchable: Boolean ) on FRAGMENT_DEFINITION其中queryName用于指定自动生成查询的名字(即文档示例中的CommentBodyRefetchQuery)。同目录下的多个生成器对应不同 Fragment 宿主类型,印证了“可重取”条件的三种情形:
viewer_query_generator.rs:Fragment 声明在Viewer类型上时生成查询;query_query_generator.rs:Fragment 声明在Query类型上时生成查询;node_query_generator.rs:Fragment 声明在实现了Node接口(有id字段)的类型上时生成查询;fetchable_query_generator.rs:对应@fetchable类型的场景。
生成的查询类型会自动导出到<queryName>.graphql.js文件,供你像示例中那样import type {CommentBodyRefetchQuery}使用。
小结
以不同数据重取 Fragment 是 Relay 构建动态、可交互 UI 的基础能力。核心要点可归纳为:
- 理解语义:重取 = 把 Fragment 放到新的查询根下、用新变量重新取数渲染;Fragment 不能独立取数,必须依托自动生成的查询。
- 使用
useRefetchableFragment:在 Fragment 上标注@refetchable(queryName: "..."),用refetch(variables, options)触发重取;变量只传需要改变的,id可省略,fetchPolicy默认'store-or-network'。 - 管理加载态:默认路径下确保组件上方有
Suspense边界;若要保留已渲染内容,可用fetchQuery预取 +'store-only'策略手动维护加载状态。 - 理解原理:编译器侧的
refetchable_fragmenttransform 负责生成查询,运行时侧的useRefetchableFragmentInternal负责变量合并、id自动补齐与请求编排,前后端共同支撑这一能力。
- 前端
- 开发工具
【免费下载链接】relay
Relay is a JavaScript framework for building>项目地址:https://gitcode.com/gh_mirrors/relay29/relay
相关推荐
goscan性能优化:如何快速扫描大型企业网络环境
goscan性能优化:如何快速扫描大型企业网络环境 goscan是一款简单高效的IPv4网络扫描工具,能够快速发现局域网内所有活跃设备。对于大型企业网络环境,网
前端开发工具Relay useFragment Hook 完全指南:从 Fragment Reference 到数据订阅与 Suspense
Relay useFragment Hook 完全指南:从 Fragment Reference 到数据订阅与 Suspense useFragment 是 R
前端开发工具BlueBubbles Server核心功能解析:从私有API到WebSocket实时通信
BlueBubbles Server核心功能解析:从私有API到WebSocket实时通信 BlueBubbles Server是BlueBubbles应用生态
前端开发工具