news 2026/9/23 9:01:12

Relay 17 指南:使用不同的数据重新获取查询(useQueryLoader / loadQuery 与 useLazyLoadQuery 完整实战)

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Relay 17 指南:使用不同的数据重新获取查询(useQueryLoader / loadQuery 与 useLazyLoadQuery 完整实战)

Relay 17 指南:使用不同的数据重新获取查询(useQueryLoader / loadQuery 与 useLazyLoadQuery 完整实战)

【免费下载链接】relayRelay is a JavaScript framework for building>项目地址: https://gitcode.com/gh_mirrors/relay29/relay

导读

本文聚焦 Relay 数据驱动 React 应用中的一个高频交互场景:"用不同的数据重新获取查询"(Refetching Queries with Different Data)——即在同一查询的基础上,通过改变查询变量(query variables)来切换当前展示的内容,例如切换当前选中的条目、渲染不同的列表,或在用户交互后过渡到新内容。文章完整覆盖useQueryLoader/loadQueryuseLazyLoadQuery两条实现路径,并深入讲解默认fetchPolicyfetchKey语义、以及两种"避免 Suspense 隐藏已渲染内容"的实操方案。阅读本文后,你将能在 Relay 17(本仓库版本化文档 v17.0.0)项目中熟练实现带切换语义的查询重取,并理解其背后的源码机制。


什么是"用不同数据重新获取查询"

当我们在 Relay 中谈论"refetching a query"(重新获取查询)时,指的是为与最初渲染时不同的数据再次获取该查询。典型场景包括:

  • 改变当前选中的条目(例如切换详情页展示的用户);
  • 渲染与当前列表不同的另一组列表项;
  • 更一般地,让当前渲染的内容过渡到新的或不同的内容。

这与 Refreshing Queries(刷新查询) 有本质区别:刷新是用完全相同的变量重新获取同一份数据,以获得服务端最新版本;而"用不同数据重新获取"则通过更换变量驱动内容切换。两者在实现上的差异仅在于调用loadQuery或更新 state 时所传入的 variables 是否变化,代码骨架高度相似,可相互对照学习。

Relay 提供了两条主流实现路径,分别对应两种数据获取模型:

  1. 预取(preload)模型useQueryLoader+loadQuery+usePreloadedQuery,参见 Fetching Queries for Render;
  2. 渲染期获取(fetch-on-render)模型useLazyLoadQuery,参见 Lazily Fetching Queries during Render。

下面分别展开。


方式一:使用useQueryLoader/loadQuery重新获取

基础用法:传入不同的查询变量

与 Refreshing Queries with useQueryLoader 类似,我们使用useQueryLoaderHook,但这一次传入的是不同的查询变量

/** * App.react.js */ const AppQuery = require('__generated__/AppQuery.graphql'); function App(props: Props) { const variables = {id: '4'}; const [queryRef, loadQuery] = useQueryLoader( AppQuery, props.appQueryRef /* initial query ref */ ); const refetch = useCallback(() => { // Load the query again using the same original variables. // Calling loadQuery will update the value of queryRef. loadQuery({id: 'different-id'}); }, [/* ... */]); return ( <React.Suspense fallback="Loading query..."> <MainContent refetch={refetch} queryRef={queryRef} /> </React.Suspense> ); }
/** * MainContent.react.js */ // Renders the preloaded query, given the query reference function MainContent(props) { const {refetch, queryRef} = props; const data = usePreloadedQuery( graphql` query AppQuery($id: ID!) { user(id: $id) { name friends { count } } } `, queryRef, ); return ( <> <h1>{data.user?.name}</h1> <div>Friends count: {data.user?.friends?.count}</div> <Button onClick={() => refetch()}> Fetch latest count </Button> </> ); }

要点解析:

  • 我们在refetch 事件处理器内调用loadQuery({id: 'different-id'}),因此网络请求会立即开始(而非等到渲染时);随后把更新后的queryRef传给usePreloadedQuery,使其渲染更新后的数据。
  • 我们没有给loadQueryfetchPolicy,因此它使用默认值'store-or-network':优先复用本地 Relay store 中已有的缓存数据,仅当查询所依赖的数据缺失时才发起网络请求。你也可以显式传入其他策略来控制是否使用本地缓存,详见 Reusing Cached Data For Render(重用缓存数据渲染)。
  • 调用loadQuery重新渲染组件,并可能导致usePreloadedQuery挂起(suspend),相关机制见 Loading States with Suspense。因此必须在包裹MainContent的位置提供Suspense边界,以便在挂起期间显示 fallback 加载态。

loadQuery 底层的 fetchKey 与默认 fetchPolicy

为什么"再调一次loadQuery"就能让usePreloadedQuery重新渲染新数据,而不是命中 React 层的缓存?答案在 packages/react-relay/relay-hooks/loadQuery.js 的实现中:

  • 每次调用都会生成新的fetchKey:源码第 66 行声明模块级计数器let fetchKey = 100001;,第 109 行在每次loadQuery时执行fetchKey++,并作为PreloadedQuery的一部分(第 415 行fetchKey)返回。源码注释明确指出:这样做的目的是确保每个新生成的 query ref 都被独立求值,避免出现"第二次调用loadQuery想触发 refetch,却被 Suspense 缓存复用了旧结果"的问题。
  • 默认fetchPolicy'store-or-network':源码第 49 行const DEFAULT_FETCH_POLICY: FetchPolicy = 'store-or-network';。当未显式提供时(第 111-114 行),使用该默认值;对于带metadata.live的 live query 或启用了执行期 resolver 的查询,默认值则会变为'store-and-network'(第 50、52-64 行)。
  • store-only时直接跳过网络层:源码第 285-288 行,创建 operation descriptor 后先environment.retain(operation)保留数据防止被 GC;若fetchPolicy === 'store-only'则直接return,不发任何网络请求。
  • store-or-network会先做可用性检查:第 293-297 行,仅当environment.check(operation).status !== 'available'时才真正发起网络请求;若 store 中已有完整可用数据,则跳过网络,直接读缓存。
  • 请求是 eager(立即执行)的:第 147-150 行使用ReplaySubject捕获 eager 执行期间的事件,再通过 Observable 回放给订阅者,确保调用loadQuery的当下网络请求就已启动。

这解释了文档中"调用loadQuery会立即发起请求、并可能触发 Suspense"的行为根源。

如果希望避免 Suspense:fetchQuery先行

有些场景下,你不希望展示 Suspense fallback——因为 fallback 会隐藏已经渲染出来的内容。此时可以改用fetchQuery,并手动维护一个 loading 状态:

/** * App.react.js */ const AppQuery = require('__generated__/AppQuery.graphql'); function App(props: Props) { const environment = useRelayEnvironment(); const [queryRef, loadQuery] = useQueryLoader( AppQuery, props.appQueryRef /* initial query ref */ ); const [isRefetching, setIsRefetching] = useState(false) const refetch = useCallback(() => { if (isRefetching) { return; } setIsRefetching(true); // fetchQuery will fetch the query and write // the data to the Relay store. This will ensure // that when we re-render, the data is already // cached and we don't suspend fetchQuery(environment, AppQuery, variables) .subscribe({ complete: () => { setIsRefetching(false); // *After* the query has been fetched, we call // loadQuery again to re-render with a new // queryRef. // At this point the data for the query should // be cached, so we use the 'store-only' // fetchPolicy to avoid suspending. loadQuery({id: 'different-id'}, {fetchPolicy: 'store-only'}); }, error: () => { setIsRefetching(false); } }); }, [/* ... */]); return ( <React.Suspense fallback="Loading query..."> <MainContent isRefetching={isRefetching} refetch={refetch} queryRef={queryRef} /> </React.Suspense> ); }

注:上述代码中的variables应替换为本次要获取的新变量(例如{id: 'different-id'}),与loadQuery中传入的变量保持一致。

要点解析:

  • 既然不挂起,就要自行跟踪isRefetchingloading 状态,用它来渲染忙碌 spinner 等加载 UI——而且是在MainContent内部渲染,从而不会隐藏MainContent本体。
  • 事件处理器中先调用fetchQuery:它负责从网络获取查询并把数据写入本地 Relay store。当fetchQuery的请求完成后,再调用loadQuery获得更新的queryRef,传给usePreloadedQuery渲染更新后的数据——整体思路与前一个示例一致,只是时序被拆成了两步。
  • 此时再调用loadQuery时,查询数据已经缓存在本地 store 中,因此使用fetchPolicy: 'store-only'避免挂起,仅从缓存中读取数据。

fetchQuery在这里承担关键角色,其语义可以从源码与 API 文档确认:

  • 位于 packages/relay-runtime/query/fetchQuery.js,其默认fetchPolicy'network-only'(第 138 行options?.fetchPolicy ?? 'network-only'),即默认总是从网络获取;
  • 它会自动把获取到的数据写入内存 Relay store,并通知订阅了相关数据的组件(参见 fetchQuery API 参考 的 Behavior 一节);
  • 不会 retain(保留)数据,请求完成后数据不保证留在 store 中;如需保留需自行调用environment.retain()
  • 它会自动对相同查询 + 相同变量且同时在途的请求做去重。

另外注意:上面的例子仍保留了外层的Suspense边界,但正常情况下重取期间usePreloadedQuery不会挂起(数据已在缓存中),因此MainContent不会被 fallback 替换,而isRefetching状态则驱动了细粒度的加载反馈。若你使用的是 React 并发渲染的未来版本,React 将提供原生选项避免用 Suspense fallback 隐藏已渲染内容(详见本文对应的 OssAvoidSuspenseNote 说明)。


方式二:使用useLazyLoadQuery重新获取

基础用法:更新 variables 与 fetchKey

与 Refreshing Queries with useLazyLoadQuery 类似,我们也可以使用useLazyLoadQuery,区别在于这次传入不同的查询变量

/** * App.react.js */ const AppQuery = require('__generated__/AppQuery.graphql'); function App(props: Props) { const [queryArgs, setQueryArgs] = useState({ options: {fetchKey: 0}, variables: {id: '4'}, }); const refetch = useCallback(() => { // Trigger a re-render of useLazyLoadQuery with new variables, // *and* an updated fetchKey. // The new fetchKey will ensure that the query is fully // re-evaluated and refetched. setQueryArgs(prev => ({ options: { fetchKey: (prev?.options.fetchKey ?? 0) + 1, }, variables: {id: 'different-id'} })); }, [/* ... */]); return ( <React.Suspense fallback="Loading query..."> <MainContent refetch={refetch} queryArgs={queryArgs} /> </React.Suspense> ); }
/** * MainContent.react.js */ // Fetches and renders the query, given the fetch options function MainContent(props) { const {refetch, queryArgs} = props; const data = useLazyLoadQuery( graphql` query AppQuery($id: ID!) { user(id: $id) { name friends { count } } } `, queryArgs.variables, queryArgs.options, ); return ( <> <h1>{data.user?.name}</h1> <div>Friends count: {data.user.friends?.count}</div> <Button onClick={() => refetch()}> Fetch latest count </Button> </> ); }

要点解析:

  • 在事件处理器中通过更新 state 中的 query args触发重取:这会让使用useLazyLoadQueryMainContent以新的variablesfetchKey重新渲染,并在渲染时重新获取查询。
  • 我们传入了一个每次递增的新fetchKey。向useLazyLoadQuery传入新的fetchKey,会确保查询被完全重新求值并重新获取
  • 我们没有传入新的fetchPolicy,因此使用默认值'store-or-network';同样可以显式指定其他策略来控制是否复用本地缓存,参见 Reusing Cached Data For Render。
  • 事件处理器中的 state 更新会触发组件重新渲染,并可能导致组件挂起(机制见 Loading States with Suspense)。因此必须在包裹MainContent的位置提供Suspense边界,以显示 fallback 加载态。

关于fetchKeyfetchPolicy的权威语义,可直接参见 packages/react-relay/relay-hooks/useLazyLoadQuery.js 中Options类型的注释:

  • fetchKey(第 52-54 行):传入不同的fetchKey会强制在当前组件重新渲染时重新求值查询与变量,即使变量没有变化、甚至组件没有重新挂载(类似给 React 组件传不同key会使其重新挂载)。如果fetchKey与上一次渲染不同,当前查询会针对 store 重新求值,并根据当前fetchPolicy与缓存状态决定是否重新获取。
  • networkCacheConfig(第 56-58 行):默认{force: true},用于绕过网络层的查询响应缓存。
  • 从实现看,useLazyLoadQuery内部通过useMemoOperationDescriptor构造 operation descriptor,再交由useLazyLoadQueryNode处理fetchKeyfetchPolicyfetchObservable(第 108-125 行),从而在渲染阶段驱动数据获取。

如果希望避免 Suspense:fetchQuery先行

useQueryLoader路径相同,useLazyLoadQuery也支持"先fetchQuery写缓存、再更新 state 读缓存"的免挂起方案:

/** * App.react.js */ const AppQuery = require('__generated__/AppQuery.graphql'); function App(props: Props) { const environment = useRelayEnvironment(); const [isRefreshing, setIsRefreshing] = useState(false) const [queryArgs, setQueryArgs] = useState({ options: {fetchKey: 0, fetchPolicy: 'store-or-network'}, variables: {id: '4'}, }); const refetch = useCallback(() => { if (isRefreshing) { return; } setIsRefreshing(true); // fetchQuery will fetch the query and write // the data to the Relay store. This will ensure // that when we re-render, the data is already // cached and we don't suspend fetchQuery(environment, AppQuery, variables) .subscribe({ complete: () => { setIsRefreshing(false); // *After* the query has been fetched, we update // our state to re-render with the new fetchKey // and fetchPolicy. // At this point the data for the query should // be cached, so we use the 'store-only' // fetchPolicy to avoid suspending. setQueryArgs(prev => ({ options: { fetchKey: (prev?.options.fetchKey ?? 0) + 1, fetchPolicy: 'store-only', }, variables: {id: 'different-id'} })); }, error: () => { setIsRefreshing(false); } }); }, [/* ... */]); return ( <React.Suspense fallback="Loading query..."> <MainContent isRefetching={isRefetching} refetch={refetch} queryArgs={queryArgs} /> </React.Suspense> ); }

注:fetchQuery(environment, AppQuery, variables)中的variables应替换为本次要获取的新变量(例如{id: 'different-id'}),与setQueryArgs中写入的变量保持一致。

要点解析:

  • 重取期间我们自行维护isRefetching状态,用它渲染忙碌 spinner 等加载 UI,而无需隐藏MainContent
  • 事件处理器中先调用fetchQuery将数据写入本地 Relay store;待请求完成后更新 state,以新的fetchKeyfetchPolicy驱动useLazyLoadQuery重新渲染出更新后的数据。
  • 此时数据已经缓存在 store 中,因此把fetchPolicy设为'store-only'避免挂起,只读取已缓存数据。

fetchPolicy 语义速查

无论是loadQueryuseLazyLoadQuery还是fetchQueryfetchPolicy都决定"何时读取缓存、何时发网络请求"。四种取值及语义整理如下(依据 useLazyLoadQuery.js 中 Options 注释与 Reusing Cached Data For Render):

fetchPolicy是否复用本地缓存是否发网络请求典型用途
store-or-network仅当缓存数据缺失时默认策略,兼顾速度与新鲜度
store-and-network总是既要缓存即时渲染,又要后台更新
network-only总是强制获取最新数据(如刷新)
store-only从不只读缓存,避免挂起;或配合fetchQuery预取后使用

注意两点默认值差异:loadQueryuseLazyLoadQuery的默认策略是'store-or-network',而fetchQuery的默认策略是'network-only'(fetchQuery.js 第 138 行)。这正是"先用fetchQuery强制拉取、再用store-only读缓存"这一组合能够成立的原因。


相关阅读

  • Refreshing Queries(刷新查询):用相同变量获取最新数据,与本文的"不同数据重取"互为对照
  • Refetching Fragments with Different Data(用不同数据重新获取 Fragment):面向 Fragment 的对应方案
  • Fetching Queries for Render(为渲染获取查询):useQueryLoaderuseLazyLoadQuery的入门讲解
  • Loading States with Suspense(Suspense 加载状态):Suspense 边界与 fallback 机制
  • Reusing Cached Data For Render(重用缓存数据):fetchPolicy 与缓存策略的完整讨论
  • fetchQuery API 参考:fetchQuery的参数、返回值与行为细节
  • useQueryLoader 与 useLazyLoadQuery:两个 Hook 的 API 参考

【免费下载链接】relayRelay is a JavaScript framework for building>项目地址: https://gitcode.com/gh_mirrors/relay29/relay

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

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

机器学习频谱感知实战:从特征提取到模型部署的完整指南

简介&#xff1a;本资源为基于机器学习的认知无线电频谱感知MATLAB仿真资料包&#xff0c;面向计算机、电子信息工程、数学等专业的大学生&#xff0c;适用于课程设计、期末大作业与毕业设计场景。包内共22个文件&#xff0c;以csv数据集、ipynb交互式代码、py脚本、pdf报告与m…

作者头像 李华
网站建设 2026/9/23 9:00:18

C++调用NI-DMM驱动数字万用表:dmm.cpp解析与实战指南

简介&#xff1a;这是一份用于NI&#xff08;National Instruments&#xff09;数字万用表&#xff08;DMM&#xff09;的C驱动源码&#xff0c;面向需要以编程方式自动控制万用表完成电压、电流、电阻等参数测量的硬件开发者或测试工程师。压缩包内仅含1个cpp源文件&#xff0…

作者头像 李华
网站建设 2026/9/23 8:59:33

苏州物流电动15吨叉车品牌推荐与广受好评的叉车制造厂家选择指南

苏州物流电动15吨叉车选购前&#xff0c;你必须先理清这些核心逻辑 随着苏州本地物流、制造业的升级&#xff0c;15吨级电动叉车正在成为港口、物流堆场、重型货场的刚需设备。不同于3吨以下的民用搬运设备&#xff0c;15吨电动叉车属于重载搬运装备&#xff0c;其选购逻辑不仅…

作者头像 李华
网站建设 2026/9/23 8:59:28

微信小程序教学辅助管理系统开发实践:从架构到答辩全流程解析

“你这个小程序项目&#xff0c;答辩的时候老师肯定会问&#xff1a;‘这个系统有什么创新点&#xff1f;’”这是我指导学弟做毕业设计时最常说的一句话。如果你选的方向是“基于微信小程序的教学辅助管理系统”&#xff0c;那这篇文章就是为你准备的。我知道&#xff0c;看到…

作者头像 李华
网站建设 2026/9/23 8:59:25

C# WinForms人脸识别打卡系统实战:从摄像头采集到考勤导出

简介&#xff1a;一套基于C#的WinForm人脸识别打卡系统源码包&#xff0c;完整覆盖考勤管理中的界面搭建、摄像头图像采集、人脸比对、打卡记录与数据库存储等核心环节&#xff0c;适合作为课程设计、毕业设计或小型企业考勤系统原型来学习。压缩包共69个文件、3.3MB&#xff0…

作者头像 李华
网站建设 2026/9/23 8:58:45

缓存后端选型实战:Redis、Memcached、Groupcache与本地缓存对比

给Templar这套接入层选缓存后端的时候&#xff0c;我确实纠结了一阵。Templar是我们内部一个业务聚合与转发服务&#xff0c;每天要承接海量读多写少的查询&#xff0c;其中很大一部分请求命中完全相同的结果&#xff0c;不缓存的话&#xff0c;下游和带宽都会被打爆。候选名单…

作者头像 李华