loadQuery是 Relay(React 数据驱动框架)提供的命令式查询预加载 API,它与usePreloadedQuery()配合实现官方推荐的 "render-as-you-fetch"(边渲染边获取)模式:在路由跳转、点击等事件发生时提前发起 GraphQL 请求,渲染时直接消费已就绪或进行中的数据。读完本文,你将掌握loadQuery的全部参数语义、三种 fetchPolicy 的取舍、query reference 的生命周期管理,以及其底层在 loadQuery.js 中的执行与去重原理,能够在真实 Relay 应用中正确实现"请求早于渲染"的数据预加载方案。
loadQuery是什么:定位与设计目标
loadQuery是 react-relay 包导出的顶层函数(见 index.js),专门用于配合usePreloadedQuery()钩子实现 "render-as-you-fetch" 模式。与useLazyLoadQuery()在组件渲染时才发起请求不同,loadQuery允许你在任意事件回调(路由导航、按钮点击、滚动触发等)中提前启动数据请求,让网络请求与组件渲染并行进行,从而缩短用户感知的加载时间。
官方文档同时给出了一个重要提醒:loadQuery返回的 query reference 会向 Relay store 泄漏数据,如果在其不再被引用时没有调用.dispose(),这些数据将一直保留在 store 中无法被垃圾回收。因此官方更推荐优先使用useQueryLoader(参见 use-query-loader.md),它会在组件卸载或引用不再可达时自动帮你处理 dispose。
基础用法:在事件中预加载查询
loadQuery的典型调用方式是"在事件中调用,而不是在组件顶层调用"。以下是最基础的示例:
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() // 注意:query reference 应当调用 .dispose(), // 本例为了简洁省略了这一步。这个示例包含了三个要点:
- 事件驱动调用:
loadQuery应该在事件回调中调用,而不是在组件渲染顶层执行,否则会违反 React 渲染阶段的规则(详见下文"行为"一节)。 - 获取到的 query reference 交给
usePreloadedQuery()消费:渲染层通过usePreloadedQuery(query, queryReference)从 Relay store 读取数据,查询进行中时会触发 Suspense 挂起,失败时抛出错误,成功时返回查询结果(完整的消费示例见 use-preloaded-query.md)。 - 手动释放:query reference 使用完毕必须调用
.dispose(),否则数据会持续被 store 保留。
与 useQueryLoader 组合的推荐写法
官方推荐的更安全的组合是用useQueryLoader包装loadQuery,由钩子负责 query reference 的自动清理(源码见 useQueryLoader.js):
import type {AppQuery as AppQueryType} from 'AppQuery.graphql'; import type {PreloadedQuery} from 'react-relay'; const {useQueryLoader, usePreloadedQuery} = require('react-relay'); const AppQuery = graphql` query AppQuery($id: ID!) { user(id: $id) { name } } `; function QueryFetcherExample(props: Props) { const [queryReference, loadQuery, disposeQuery] = useQueryLoader( AppQuery, props.initialQueryRef, /* 例如由路由提供 */ ); if (queryReference == null) { return ( <Button onClick={() => loadQuery({id: '4'})}>点击显示姓名</Button> ); } return ( <> <Button onClick={disposeQuery}>点击隐藏并释放该查询</Button> <React.Suspense fallback="Loading"> <NameDisplay queryReference={queryReference} /> </React.Suspense> </> ); } function NameDisplay({queryReference}) { const data = usePreloadedQuery<AppQueryType>(AppQuery, queryReference); return <h1>{data.user?.name}</h1>; }useQueryLoader返回的loadQuery回调在内部就是调用loadQuery并把结果存入 React state(见 useQueryLoader.js),同时通过undisposedQueryReferencesRef追踪所有未被提交的 query reference,在新引用提交时统一释放旧引用,组件卸载时清空全部剩余引用,从根本上避免数据泄漏。
参数详解
loadQuery接收四个参数,其中前三个为必需,后两个可选:
environment
类型:IEnvironment。用于执行请求的 Relay Environment 实例。如果你在 React 组件内部发起请求,通常应该使用useRelayEnvironment()获取的环境(参见 use-relay-environment.md),以保证与渲染上下文一致。从源码看,loadQuery对该环境执行environment.check()、environment.retain()、environment.executeWithSource()等操作(loadQuery.js),因此环境实例的 store 与网络层配置直接决定预加载行为。
query
类型:GraphQLTaggedNode(graphql模板字面量)或PreloadableConcreteRequest(预加载的具体请求)。
- 使用
graphql模板字面量声明查询,Relay 编译器会将其编译为 ConcreteRequest。 - 或者使用可预加载的具体请求:通过
require引入<name-of-query>$Parameters.graphql文件获取。只有查询标注了@preloadable指令时,Relay 编译器才会生成$Parameters文件。这种形式允许在查询 AST(代码)尚未下载完成时,就基于持久化查询 ID 先行发起网络请求。
variables
类型:QueryType<TQuery>['variables']。包含查询所需的变量值对象,必须与查询内部声明的 GraphQL 变量一一匹配。
options(可选)
可选配置对象,包含以下键:
| 键 | 类型 | 默认值 | 说明 |
|---|---|---|---|
fetchPolicy | 'store-or-network' \| 'store-and-network' \| 'network-only' | 'store-or-network' | 决定是否复用本地缓存数据,以及是否发送网络请求 |
networkCacheConfig | CacheConfig | {force: true} | 网络层的缓存配置 |
fetchPolicy的三种取值语义如下(详见官方 fetch-policies.md 与 availability-of-data.md 指南):
"store-or-network"(默认):复用本地缓存数据,且仅当查询的某些数据缺失时才发送网络请求。如果查询完全命中缓存,则不发起网络请求。"store-and-network":复用本地缓存数据,并且无论缓存是否缺失都会发送网络请求。"network-only":不复用任何本地缓存数据,总是发送网络请求,完全忽略 Relay store 中可能存在的缓存。
networkCacheConfig:网络层缓存配置。Relay 网络层可能额外维护一个查询响应缓存,对完全相同的查询复用网络响应。若想完全绕过该缓存(默认行为即是如此),传入{force: true}。事实上,从实现源码看,无论你是否传入该选项,loadQuery都会强制合并force: true(见 loadQuery.js):
const networkCacheConfig = { ...options?.networkCacheConfig, force: true, };这意味着loadQuery预加载的网络请求默认总是穿透网络层响应缓存,避免复用过期响应;你仍可通过该选项传入其他网络层配置项(如poll、metadata等)来定制行为。
environmentProviderOptions(可选)
类型:TEnvironmentProviderOptions。传递给environmentProvider的选项对象,用于 EntryPoint 体系中的prepareSurfaceEntryPoint.js场景——即由环境提供方决定最终使用哪个环境执行请求。
Flow 类型参数
TQuery:应与指定查询的 Flow 类型对应。该类型可从自动生成的文件<query_name>.graphql.js中导入。TEnvironmentProviderOptions:environmentProviderOptions参数的类型。
从类型定义看(EntryPointTypes.flow.js),LoadQueryOptions还包含一个内部使用的__nameForWarning字段(用于开发警告中的查询命名),属于内部细节,一般无需关注:
export type LoadQueryOptions = { readonly fetchPolicy?: ?FetchPolicy, readonly networkCacheConfig?: ?CacheConfig, readonly __nameForWarning?: ?string, };返回值:query reference
loadQuery返回一个 query reference,其唯一稳定可用的属性是:
dispose:释放 query reference 在 store 中的保留(retention)。调用后,该 query reference 引用的数据可能被垃圾回收。
文档特别强调:返回值的精确格式是不稳定且极可能变化的。强烈建议不要使用返回值上的任何其他属性,因为这类代码在升级 Relay 时极容易破坏。正确的做法是把loadQuery()的结果直接传给usePreloadedQuery()。
不过,从当前源码(loadQuery.js 与 EntryPointTypes.flow.js)可以看到 query reference 的实际内部结构,理解它有助于把握生命周期:
| 属性 | 说明 |
|---|---|
dispose() | 释放查询数据并取消仍在进行中的网络请求(releaseQuery + cancelNetworkRequest) |
releaseQuery() | 仅释放查询数据(解除 store 保留) |
cancelNetworkRequest() | 仅取消仍在进行中的网络请求 |
fetchKey | 每次调用loadQuery递增的唯一键,确保每个 query reference 被独立求值 |
fetchPolicy | 本次调用使用的 fetch policy |
id | 查询 ID(持久化查询 ID 或缓存 ID) |
isDisposed | 是否已释放(getter) |
networkError | 网络请求失败时的错误对象(getter) |
name | 查询名称 |
networkCacheConfig | 本次调用的网络层缓存配置 |
source | 网络事件的 Observable 源(无网络请求时为 undefined) |
variables | 本次调用的查询变量 |
environment | 执行请求的 Relay 环境 |
行为与生命周期细节
官方文档明确了loadQuery的三条核心行为,前两条可以从源码中得到印证:
1. 数据写入 store 的时机
loadQuery()传入查询时会获取数据;传入预加载的具体请求(preloadable concrete request)时则会同时获取数据与查询定义。一旦查询和数据都可用,数据就会被写入 store。这与preloadQuery_DEPRECATED不同——旧 API 只有在查询被传入usePreloadedQuery时才会把数据写入 store。
2. 数据保留与垃圾回收
loadQuery返回的 query reference 会被 Relay store保留(retain),从而防止其数据被垃圾回收。一旦你对 query reference 调用.dispose(),它就不再被保留,数据随即可能被回收。
源码中对应environment.retain(operation)调用(loadQuery.js),返回的retainReference在releaseQuery中被dispose()。useQueryLoader之所以推荐,正是因为它把"释放"这件事自动化了。
3. 渲染阶段调用会抛错
loadQuery()如果在React 渲染阶段被调用,会抛出错误。useQueryLoader返回的loadQuery回调同样有此限制,而disposeQuery也不应在渲染阶段调用。这是 React 的规则约束:副作用(发起网络请求、修改状态)只能在事件回调或 effect 中发生。
补充:fetchKey 与独立求值
每次调用loadQuery都会生成新的fetchKey(全局自增,见 loadQuery.js)。这确保每个创建出来的 query reference 都会被独立求值——即使它们对应相同的查询与变量。具体来说,它避免了这种场景:第二次调用loadQuery试图重新拉取同一查询时,usePreloadedQuery内部的 Suspense 缓存错误地复用了旧结果,而不是重新求值新引用并触发必要的重新获取。
补充:环境不一致时的回退
当 query reference 被传入与创建时不同的环境上下文中时,usePreloadedQuery会发出警告并回退到"在渲染时用新环境重新执行查询"(见 usePreloadedQuery.js),该场景未来版本会升级为硬错误,实践中应保持环境一致。
源码级原理:loadQuery 内部执行链路
从 loadQuery.js 的实现可以还原loadQuery的完整执行链路,这有助于深入理解其行为:
- 生成新
fetchKey,保证引用的独立求值。 - 解析 fetchPolicy:若未显式传入,会根据请求类型推断默认值——live 查询或启用执行期 resolver 的查询默认使用
'store-and-network',纯客户端查询(无服务端操作可拉取)使用'store-only',其余使用'store-or-network'(loadQuery.js)。 - 构造 operation 并保留:通过
createOperationDescriptor创建操作描述符,随后environment.retain(operation)保留该操作对应的数据。 - 检查缓存可用性:若 fetchPolicy 允许命中缓存,则调用
environment.check(operation);仅当状态非'available'(即缓存缺失)时才执行获取(loadQuery.js)。这正是'store-or-network'语义的落地实现。 - 两层去重:
- 网络层使用
fetchQueryDeduped(environment, 'raw-network-request-' + identifier, ...)对原始网络请求去重——保证同一 (environment, identifier) 对同时只有一个活跃请求;当查询 AST 尚未就绪时,多次调用仍可共用同一个网络请求(loadQuery.js)。 - 操作执行层再次使用
fetchQueryDeduped对操作执行去重,并为 Suspense 基础设施跟踪活跃操作状态,避免对同一响应重复处理(loadQuery.js)。
- 网络层使用
- 急切执行 + ReplaySubject 重放:与通常惰性的 Observable 不同,
loadQuery希望在调用时立即启动请求。实现上用ReplaySubject捕获急切执行期间发生的事件,再通过返回的 Observable 将事件重放给订阅者(loadQuery.js)。这也是usePreloadedQuery能拿到source并复用网络事件、避免二次请求的关键。 - 预加载查询的模块注册:当传入
PreloadableConcreteRequest(kind 为'PreloadableConcreteRequest')时,通过PreloadableQueryRegistry查找已加载的查询模块;若模块尚未加载(代码分割场景),则立即启动网络请求,并注册onLoad回调,待模块加载完成后执行写 store 与执行操作(loadQuery.js)。
这套设计回答了"为什么 loadQuery 能比 useLazyLoadQuery 更早发起请求":它绕过了渲染时机的限制,在事件回调中就完成请求发起、缓存检查与 store 写入,渲染时usePreloadedQuery只需从 store 读取即可。
何时用 loadQuery,何时用 useQueryLoader
两者关系可总结为:
useQueryLoader:React 钩子,把 query reference 存在 state 中,自动管理 dispose。适合大多数组件内按需加载的场景(点击按钮、条件渲染查询)。其loadQuery回调与disposeQuery回调都有防泄漏保证。loadQuery(直接调用):纯命令式函数,不依赖 React 生命周期,适合在路由层、EntryPoint、事件总线等非组件上下文中预加载。代价是你必须亲自管理.dispose(),否则 store 中的数据会泄漏。
官方建议:能使用useQueryLoader时就优先使用它;只有在需要脱离组件上下文预加载(如路由切换、EntryPoint 的getPreloadProps中)时,才直接调用loadQuery,并务必在不再使用 query reference 时调用.dispose()。
相关参考
- 消费端 API:usePreloadedQuery
- 自动管理生命周期的钩子:useQueryLoader
- render-as-you-fetch 完整指南:Rendering Queries
- 缓存复用策略:Fetch Policies 与 数据可用性
- 核心实现源码:loadQuery.js、useQueryLoader.js、usePreloadedQuery.js、EntryPointTypes.flow.js
- 前端
- 开发工具
【免费下载链接】relay
Relay is a JavaScript framework for building>项目地址:https://gitcode.com/gh_mirrors/relay29/relay
相关推荐
Stellar标签组件完全手册:30+内置插件的创意用法与实战案例
Stellar标签组件完全手册:30+内置插件的创意用法与实战案例 Stellar是一款综合型hexo主题,集成了博客、知识库、专栏和笔记功能,内置了30多种标
前端开发工具Relay 的 loadEntryPoint:以命令式预加载实现 render-as-you-fetch 模式
Relay 的 loadEntryPoint:以命令式预加载实现 render as you fetch 模式 loadEntryPoint 是 React R
前端开发工具Relay 中 loadQuery 的完整指南:render-as-you-fetch 数据预取与 query reference 生命周期管理
Relay 中 loadQuery 的完整指南:render as you fetch 数据预取与 query reference 生命周期管理 loadQue
前端开发工具