Relay 14 指南:使用 useRefetchableFragment 以不同数据重取 Fragment
2026/9/21 1:36:33 网站建设 项目流程

本篇技术指南围绕 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 上,即声明在ViewerQuery类型上,或声明在任何实现了Node接口(即拥有id字段)的类型上,以及@fetchable类型上。
    • 它返回一个refetch函数,该函数已被 Flow 类型化,其参数类型正好是生成的查询所期望的变量集合。
    • 它接受两个 Flow 类型参数:第一个是自动生成查询的类型(本例为CommentBodyRefetchQuery),第二个通常可以被自动推断,只需传下划线_
  • 调用refetch需要两类输入
    • 第一个参数是新变量集合。传入一组新变量会让 Fragment 以这组新变量重新取数。你只需提供@refetchable查询所需变量的一个子集:如果 Fragment 所在类型有id字段,查询会要求一个id;其余变量则是 Fragment 内被传递引用的那些变量。本例中我们传入了当前评论的id和新的lang变量值,以取回翻译后的正文。
    • 第二个是可选的 options 参数,本例未传,因此使用默认的fetchPolicy'store-or-network')——如果该 Fragment 的新数据已缓存,则跳过网络请求(参见 Reusing Cached Data For Render 相关章节)。
  • 调用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()可取消本次重取请求。
  • options(可选对象)
    • fetchPolicy:决定是否使用缓存数据、以及在缓存可用时何时发起网络请求(完整规范见 Fetch Policies)。
    • onComplete:每当重取请求完成时(包括任何增量数据载荷)都会被调用的回调。

useRefetchableFragment还具备与useFragment相同的订阅语义:组件自动订阅 Fragment 数据更新,如果该对象的数据在应用其他位置被更新(如新取数或 mutation),组件会自动用最新数据重新渲染;当 Fragment 的某些数据缺失且正在被父查询获取时,组件会挂起(suspend)。

与旧版RefetchContainer的差异

useRefetchableFragment的实现历史与 API 设计看,它与 class 时代的RefetchContainer相比主要有三点变化:

  • 无需再手写 refetch 查询,Relay 通过@refetchableFragment 自动生成;
  • 不再区分语义模糊的refetchVariablesrenderVariables,重取总是以你提供的变量正确地取数并渲染(省略的变量回退到父查询原值);
  • 重取总是明确地更新组件,而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时,源码把parentVariablesfragmentVariables与你传入的providedRefetchVariables合并,得到最终的重取变量集合——这正是“省略的变量回退到父查询原值”的实现依据;
    • 当 Fragment 有标识字段(id等)而你未显式传入时,源码会从当前 Fragment 数据中读取identifierValue自动补上,这就是“传id是可选的”的实现依据;
    • 重取请求通过createOperationDescriptor创建(强制force: true),再经loadQuery启动网络请求(必要时),随后在渲染阶段通过QueryResource.prepare读取/等待结果,并在重取查询仍在途时挂起——对应“调用 refetch 可能让组件 suspend”的行为;
    • refetch返回{dispose: disposeQuery},即文档中的 disposable,可用于取消本次重取。
  • __DEV__模式下,实现还带有一组校验函数(checkSameIDAfterRefetchcheckSameTypeAfterRefetch),会在重取后检查返回对象的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 的基础能力。核心要点可归纳为:

  1. 理解语义:重取 = 把 Fragment 放到新的查询根下、用新变量重新取数渲染;Fragment 不能独立取数,必须依托自动生成的查询。
  2. 使用useRefetchableFragment:在 Fragment 上标注@refetchable(queryName: "..."),用refetch(variables, options)触发重取;变量只传需要改变的,id可省略,fetchPolicy默认'store-or-network'
  3. 管理加载态:默认路径下确保组件上方有Suspense边界;若要保留已渲染内容,可用fetchQuery预取 +'store-only'策略手动维护加载状态。
  4. 理解原理:编译器侧的refetchable_fragmenttransform 负责生成查询,运行时侧的useRefetchableFragmentInternal负责变量合并、id自动补齐与请求编排,前后端共同支撑这一能力。
  • 前端
  • 开发工具

【免费下载链接】relay

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

点击查看免费下载
上一篇:DiffusionDet训练完全指南:从数据准备到模型优化
下一篇:如何快速使用霞鹜臻楷:面向新手的完整字体安装指南

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询