Relay 的 loadQuery:以命令式预加载实现 render-as-you-fetch 的完整指南
2026/9/22 11:11:14 网站建设 项目流程

Relay 的 loadQuery:以命令式预加载实现 render-as-you-fetch 的完整指南

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

loadQuery是 Relay 数据获取体系中的命令式入口,用于在 React 渲染之外主动发起查询请求、将查询引用(Query Reference)保留在 Relay store 中,并与usePreloadedQuery()搭配实现 "render-as-you-fetch"(边取边渲染)模式。本文以 Relay 仓库中 loadQuery API 参考文档 为主体骨架,结合 loadQuery 实现源码、类型定义与测试用例,系统讲解其参数语义、返回结构、底层执行流程与内存管理注意事项,帮助你掌握在路由导航、点击等事件回调中提前加载数据、随后在组件内同步消费的完整实战方案。

loadQuery是什么

loadQueryreact-relay包导出的一个命令式函数。与useLazyLoadQuery这类"渲染时才发起请求"的 Hook 不同,它允许你在组件渲染之前、在事件回调中主动发起查询,从而提前并行地拉取数据,这正是 "render-as-you-fetch" 的核心思想:

  • 调用loadQuery()立即开始取数;
  • 把返回的查询引用(query reference)传给usePreloadedQuery(),在渲染时读取 store 中的数据;
  • 数据未就绪时,usePreloadedQuery会触发 Suspense 挂起,就绪后返回查询结果。

官方文档明确建议loadQuery与 usePreloadedQuery 搭配使用,同时优先推荐useQueryLoader,因为它会在组件卸载或引用不再需要时自动对查询引用调用.dispose(),避免手动管理内存泄漏。参见 useQueryLoader 文档。

需要特别强调的是:如果从loadQuery返回的查询引用没有被调用.dispose(),它会持续把数据保留(retain)在 Relay store 中,造成数据泄漏、无法被垃圾回收。这是使用本 API 时必须始终铭记的一个前提。

基本用法

文档给出的最简示例(见 load-query.md):

const MyEnvironment = require('MyEnvironment'); const {loadQuery} = require('react-relay'); const query = graphql` query AppQuery($id: ID!) { user(id: $id) { name } } `; // 注意:一般不应在模块顶层调用 loadQuery, // 而应在事件(路由导航、点击等)中调用。 const queryReference = loadQuery( MyEnvironment, query, {id: '4'}, {fetchPolicy: 'store-or-network'}, ); // 稍后:把 queryReference 传给 usePreloadedQuery() // 注意:查询引用应当调用 .dispose(),本例中省略了。

两个关键约定:

  1. 不要在渲染阶段调用loadQuery()如果在 React 的 render phase 中被调用会直接抛错(文档 "Behavior" 一节明确说明,loadQueryuseQueryLoader返回的loadQuery回调都会在 render 阶段抛错)。它应放在路由导航、按钮点击等事件回调中。
  2. 及时 dispose:示例注释专门提醒"查询引用应当调用.dispose()",只是因为演示而省略。真实代码中应通过useQueryLoader自动管理,或在组件卸载时手动释放。

与之配合的消费端完整示例(来自 use-preloaded-query.md):

const React = require('React'); const {graphql, useQueryLoader, usePreloadedQuery} = require('react-relay'); const AppQuery = graphql` query AppQuery($id: ID!) { user(id: $id) { name } } `; function NameLoader(props) { const [queryReference, loadQuery] = useQueryLoader( AppQuery, props.initialQueryRef, /* 例如由 router 提供 */ ); return (<> <Button onClick={() => loadQuery({id: '4'})} disabled={queryReference != null} > Reveal your name! </Button> <Suspense fallback="Loading..."> {queryReference != null ? <NameDisplay queryReference={queryReference} /> : null } </Suspense> </>); } function NameDisplay({queryReference}) { const data = usePreloadedQuery(AppQuery, queryReference); return <h1>{data.user?.name}</h1>; }

在这个模式中,点击按钮时loadQuery({id: '4'})立即取数,渲染层通过usePreloadedQuery消费,查询挂起期间显示 Suspense fallback。相比useLazyLoadQuery,这允许更早开始取数,同时不阻塞渲染。

参数详解

environment

一个 Relay Environment 实例,用于执行请求。如果请求是在某个 React 组件内部发起的,通常应该使用useRelayEnvironment获取当前环境:

const environment = useRelayEnvironment();

query

要获取的 GraphQL 查询,有两种指定方式:

  • 使用graphql模板字面量声明的查询;
  • 或者一个可预加载的具体请求(preloadable concrete request),通过 require<name-of-query>$Parameters.graphql文件获得。

需要强调的是:Relay 编译器只有在查询标注了@preloadable指令时,才会生成$Parameters文件。也就是说,想让loadQuery拿到"先发网络请求、后加载查询 AST"的能力,查询必须声明为@preloadable

const query = graphql` query AppQuery($id: ID!) @preloadable { user(id: $id) { name } } `;

这一约束在实现源码中也有印证:loadQuery通过PreloadableQueryRegistry查询已加载的模块,若传入的是PreloadableConcreteRequest且 AST 尚未同步可用,它会立即发起网络请求,并注册onLoad回调等待 AST 加载完成后补做 store 写入与执行(见 loadQuery.js);同时源码对缺少 persisted query id 的可预加载请求会抛出Relay: \loadQuery` requires that preloadable query ... has a persisted query id` 的 invariant 断言(见 loadQuery.js),对应测试用例也注释了"Only queries with an ID are preloadable"(见 loadQuery-test.js)。

variables

包含查询变量的对象,必须与查询内部声明的 GraphQL 变量一一匹配。例如上例中查询声明了$id: ID!,调用时就要传{id: '4'}

options(可选)

可选的选项对象,包含以下键:

fetchPolicy:决定缓存与网络请求策略

控制是否复用本地缓存数据,以及基于 Relay store 中当前缓存数据的可用性是否发起网络请求。具体取值可参考 Fetch Policies 指南:

取值语义是否复用本地缓存是否发起网络请求
"store-or-network"(默认)复用本地缓存,仅当查询有数据缺失时才发起网络请求;若查询已完整缓存,则不发起网络请求有数据缺失时才发起
"store-and-network"复用本地缓存,并且总是发起网络请求,无论本地缓存是否缺失总是发起
"network-only"不复用本地缓存,总是发起网络请求拉取查询,忽略本地已有数据总是发起

此外,fetch-policies.md 还补充了第 4 种策略"store-only":只复用本地缓存、从不发起网络请求,适合读取和操作纯本地数据,或由调用方自行负责取数。该策略也出现在loadQuery的默认策略推断中——见下文"源码级原理"小节。

数据是否"缺失/过期"的判定细节可参考 Availability of Data、Presence of Data 与 Staleness of Data 指南。

networkCacheConfig(可选)

默认值为{force: true}。包含网络层的缓存配置。注意:网络层可能带有额外的查询响应缓存,会为完全相同的查询复用网络响应。默认行为是彻底绕过该缓存,也就是传{force: true}。在实现中,该默认值被硬编码合并进选项——源码第 115-118 行总是把force: true合并进networkCacheConfig(见 loadQuery.js),测试中也能看到传给executeWithSource的 operation 其cacheConfig恒为{force: true}(见 loadQuery-test.js)。

environmentProviderOptions(可选)

透传给prepareSurfaceEntryPoint.jsenvironmentProvider的选项对象。类型定义为EnvironmentProviderOptions = {readonly [string]: unknown, ...}(见 EntryPointTypes.flow.js),它只是被携带在返回的查询引用上,供 EntryPoint 环境提供方使用。

返回值:查询引用(Query Reference)

loadQuery返回一个查询引用,官方 API 文档中明确保证可用/推荐的属性只有一个:

  • dispose:释放查询引用被 store 保留(retain)的能力。调用后,该查询引用所引用的数据就可能被垃圾回收。

文档同时给出强烈警告:返回值的具体格式不稳定、未来很可能变化,强烈不建议使用其他任何属性,否则升级 Relay 版本时极易出错。正确姿势是把loadQuery()的结果直接传给usePreloadedQuery()

不过,从 EntryPointTypes.flow.js 的类型定义和 loadQuery.js 的实际返回对象看,该引用还包含以下内部属性(仅供理解实现,不建议业务依赖):

属性说明
kind固定为'PreloadedQuery'
environment发起请求的 Environment
fetchKey每次调用loadQuery递增的唯一键,用于让usePreloadedQuery的 Suspense 缓存区分同查询的不同引用
fetchPolicy本次生效的 fetch 策略
id/name查询的持久化 id 与名称
networkCacheConfig网络层缓存配置(恒含force: true
variables查询变量
networkError网络请求失败时保存的错误(getter)
isDisposed是否已释放(getter)
source网络/执行事件的可观察流(Observable),用于订阅取数进度
dispose/releaseQuery/cancelNetworkRequest释放保留、仅释放数据、仅取消进行中的网络请求
environmentProviderOptions透传的环境提供方选项

其中releaseQuery只释放数据保留,cancelNetworkRequest只取消在途网络请求,dispose则同时执行两者且幂等(见 loadQuery.js)。

TypeScript 侧的签名同样只对外暴露environment / preloadableRequest / variables / options / environmentProviderOptions五个参数并返回PreloadedQuery(见 loadQuery.d.ts)。

行为语义(Behavior)

取数与写库时机

  • loadQuery()传入普通查询时,会直接获取数据;传入可预加载的具体请求时,会同时获取数据与查询 AST。
  • 一旦查询和数据都可用,查询返回的数据就会被写入 store。这一点与preloadQuery_DEPRECATED不同——后者只有在查询被传给usePreloadedQuery时才会把数据写入 store。
  • 对应的测试用例覆盖了"数据到达后写入 store"与"dispose 后不再写库"的行为(见 loadQuery-test.js)。

数据保留与垃圾回收

  • loadQuery返回的查询引用会被 Relay store保留(retain),防止其数据被垃圾回收。
  • 一旦对查询引用调用.dispose(),数据就可能被垃圾回收。

测试中对environment.retain的断言贯穿始终——即使在store-or-network且 store 可完整满足查询(不发起任何网络请求)的情况下,查询也仍然会被 retain(见 loadQuery-test.js),这与文档"查询引用会被保留"的描述完全一致。

渲染阶段调用会抛错

loadQuery()如果在 React 渲染阶段被调用,会抛出错误。因此应始终在事件回调中使用。

源码级原理:loadQuery内部是如何工作的

结合 loadQuery.js 的实现,可以还原loadQuery的完整执行链路,帮助你更准确地理解其行为:

1. 生成新的 fetchKey

每次调用loadQueryfetchKey都会递增(见 loadQuery.js)。这样即使对同一个查询、同样的变量多次调用,每个查询引用也会被usePreloadedQuery独立求值,避免 Suspense 缓存复用旧结果而跳过必要的 refetch。

2. 推断默认 fetchPolicy

如果没有显式传入fetchPolicy,源码会按以下规则推断默认值(见 loadQuery.js):

  • 若查询带livemetadata,或启用了执行期解析器(exec-time resolvers),默认策略为'store-and-network'(常量DEFAULT_LIVE_FETCH_POLICY);
  • 若查询既无id也无text(即纯客户端查询),默认策略为'store-only'
  • 其余情况默认'store-or-network'DEFAULT_FETCH_POLICY)。

测试用例'uses the exec-time default when the available AST enables exec-time resolvers'验证了执行期解析器场景下默认策略变为store-and-network且确实发起了网络请求(见 loadQuery-test.js)。

3. 检查 store 可用性并决定是否取数

通过environment.check(operation)判断操作能否被 store 满足。逻辑为:只要策略不是store-or-network,或者environment.check(...)返回状态不是available,就发起执行(见 loadQuery.js)。源码注释特别指出,environment.check可能通过 missing field handlers 触发 store 更新,因此短路判断可以避免不必要的更新。

4. 网络请求去重

无论原始网络请求还是操作执行,都通过fetchQueryDeduped(environment, identifier)去重——同一时间只有一条在途请求(见 loadQuery.js)。原始网络请求使用'raw-network-request-' + getRequestIdentifier(params, variables)作为额外键,与操作执行的去重区分开。这样即使查询 AST 尚未加载、loadQuery被多次调用,网络请求也只会发一次。

5. 可预加载请求的特殊路径

当传入的是PreloadableConcreteRequest且查询 AST 尚未同步可用时,loadQuery会立即发起网络请求(只要策略不是store-only),并通过PreloadableQueryRegistry.onLoad(queryId, callback)注册回调;AST 加载完成后,再创建 operation、retain 并把网络响应接入 store 执行(见 loadQuery.js)。这正是@preloadable查询可以实现"代码与数据并行加载"的根本原因。对应测试覆盖了 AST 不可用时的网络请求、onLoad 回调执行、dispose 行为等(见 loadQuery-test.js)。

6. 非惰性执行与 ReplaySubject

与常规 Observable 的惰性执行不同,loadQuery希望在调用时立即开始取数。实现使用中间层ReplaySubject捕获急切执行期间产生的事件,再把这些事件重放到最终返回给调用方的 Observable 上(见 loadQuery.js)。dispose时若已进入执行阶段则退订执行流,否则退订网络请求流,并调用PreloadableQueryRegistrycancelOnLoadCallback(见 loadQuery.js)。

实战要点与注意事项

综合文档与源码,使用loadQuery时应遵循以下要点:

  1. 优先使用useQueryLoader:它会替你在组件卸载或引用不再可访问时自动 dispose 查询引用,避免数据泄漏。只有在你需要完全掌控生命周期(例如路由层面预取、EntryPoint 场景)时才直接使用loadQuery并手动管理dispose
  2. 在事件回调中调用loadQuery以及useQueryLoaderloadQuery回调都会在 React 渲染阶段抛错,务必在点击、路由导航等事件处理器中调用。
  3. 选择正确的 fetchPolicy:导航回看场景用默认的store-or-network即可快速展示缓存;需要"先显示缓存再后台刷新"用store-and-network;需要绝对新鲜数据用network-only;纯客户端数据用store-only
  4. 按需使用@preloadable:只有标注@preloadable的查询才会生成$Parameters.graphql文件,从而支持代码分割与数据请求并行发起;普通graphql字面量查询也能传给loadQuery,但走的是 AST 同步可用的路径。
  5. 不要依赖返回值的内部结构:查询引用的精确格式不稳定,仅把loadQuery的结果交给usePreloadedQuery,需要释放时调用.dispose()
  6. 理解数据写入时机:与preloadQuery_DEPRECATED不同,loadQuery在数据到达后立即写入 store,因此即便查询引用尚未被组件消费,store 中也已包含数据,Suspense 恢复渲染时可以直接读取。

关联资源

如需深入掌握相关 API 与机制,可继续阅读仓库内以下资料:

  • API 文档:loadQuery、usePreloadedQuery、useQueryLoader
  • 指南:Fetch Policies、Availability of Data、Presence of Data、Rendering Queries
  • 实现源码:loadQuery.js、EntryPointTypes.flow.js、loadQuery.d.ts
  • 测试用例:loadQuery-test.js、loadQuery-store-behavior-test.js、loadQuery-source-behavior-test.js

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

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

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

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

立即咨询