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是什么
loadQuery是react-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(),本例中省略了。两个关键约定:
- 不要在渲染阶段调用:
loadQuery()如果在 React 的 render phase 中被调用会直接抛错(文档 "Behavior" 一节明确说明,loadQuery与useQueryLoader返回的loadQuery回调都会在 render 阶段抛错)。它应放在路由导航、按钮点击等事件回调中。 - 及时 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.js中environmentProvider的选项对象。类型定义为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
每次调用loadQuery,fetchKey都会递增(见 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时若已进入执行阶段则退订执行流,否则退订网络请求流,并调用PreloadableQueryRegistry的cancelOnLoadCallback(见 loadQuery.js)。
实战要点与注意事项
综合文档与源码,使用loadQuery时应遵循以下要点:
- 优先使用
useQueryLoader:它会替你在组件卸载或引用不再可访问时自动 dispose 查询引用,避免数据泄漏。只有在你需要完全掌控生命周期(例如路由层面预取、EntryPoint 场景)时才直接使用loadQuery并手动管理dispose。 - 在事件回调中调用:
loadQuery以及useQueryLoader的loadQuery回调都会在 React 渲染阶段抛错,务必在点击、路由导航等事件处理器中调用。 - 选择正确的 fetchPolicy:导航回看场景用默认的
store-or-network即可快速展示缓存;需要"先显示缓存再后台刷新"用store-and-network;需要绝对新鲜数据用network-only;纯客户端数据用store-only。 - 按需使用
@preloadable:只有标注@preloadable的查询才会生成$Parameters.graphql文件,从而支持代码分割与数据请求并行发起;普通graphql字面量查询也能传给loadQuery,但走的是 AST 同步可用的路径。 - 不要依赖返回值的内部结构:查询引用的精确格式不稳定,仅把
loadQuery的结果交给usePreloadedQuery,需要释放时调用.dispose()。 - 理解数据写入时机:与
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),仅供参考