Relay 命令式数据获取:fetchQuery 完整 API 指南与源码剖析
fetchQuery 是 React Relay 提供的一个命令式(imperative)数据获取 API,允许你在 React 组件之外(例如事件回调、非 React 代码、导航逻辑)发起 GraphQL 查询,并得到一个可订阅的 Observable 对象。本文以 Relay v13 官方 API 文档 为骨架,结合仓库中 fetchQuery.js 与 fetchQueryInternal.js 的源码实现,系统讲解其参数、返回值、事件语义、去重机制与数据保留策略,帮助你安全、正确地把它用在真实项目中。
为什么需要 fetchQuery
在 Relay 中,绝大多数数据获取发生在 React 渲染阶段,通过 useLazyLoadQuery、useQueryLoader 等 Hook 完成。但实际业务里总有一些场景需要在 React 之外主动发起请求,例如:
- 页面路由跳转前预取数据;
- 点击事件、定时任务、Web Worker 回调中获取数据;
- 将数据写入全局状态后由组件消费;
- 需要精细控制请求生命周期(订阅、取消)的副作用代码。
fetchQuery 正是为这类场景设计:它执行一次查询、把结果写入 Relay store、并返回一个可观察对象,让你用订阅式(observable)或 Promise 式(toPromise)两种风格消费数据。
基本用法:订阅一个查询
fetchQuery 从 react-relay 导出,核心签名是 fetchQuery(environment, query, variables, options?)。最简单的调用方式是传入环境、graphql 模板字面量定义的查询以及变量,然后调用返回 Observable 的 .subscribe():
import type {AppQuery} from 'AppQuery.graphql';
// 建议优先传入 useRelayEnvironment() 返回的环境
const MyEnvironment = require('MyEnvironment');
const {fetchQuery} = require('react-relay');
fetchQuery<AppQuery>(
environment,
graphql`
query AppQuery($id: ID!) {
user(id: $id) {
name
}
}
`,
{id: 4},
)
.subscribe({
start: () => {...},
complete: () => {...},
error: (error) => {...},
next: (data) => {...}
});
几点需要特别说明:
- 类型导入
import type {AppQuery} from 'AppQuery.graphql'中的AppQuery是 Relay 编译器自动生成的类型,通过它fetchQuery可以静态校验返回数据的形状以及variables是否符合查询声明的 GraphQL 变量类型(详见下文"类型参数 TQuery"一节)。 - 如果你在 React 组件内部发起请求,建议通过
useRelayEnvironment获取当前环境,而不是使用全局单例,以保证多环境(例如多租户、测试环境)下的正确性。 - 从源码看,
fetchQuery会先通过getRequest(query)取得查询节点,并用invariant断言queryNode.params.operationKind === 'query',即只接受 query 操作;传入 mutation 或 subscription 会直接抛出'fetchQuery: Expected query operation'错误。
参数详解
environment(必需)
要执行请求的 Relay Environment 实例。如上文所述,若请求在 React 组件内发起,请使用 useRelayEnvironment 返回的环境;若在组件外,则传入你自己创建或全局共享的环境。
query(必需)
使用 graphql 模板字面量定义的 GraphQL 查询。变量需要在查询体内声明,并在 variables 参数中传入对应的值。
variables(必需)
包含查询变量的对象。这些变量的键与类型必须与查询内声明的 GraphQL 变量一致,TQuery 类型参数会保证这一点在编译期得到校验。
options(可选)
一个可选的 options 对象,目前支持以下配置:
| 配置项 | 类型 | 说明 |
|---|---|---|
networkCacheConfig |
Object | 网络缓存配置对象 |
networkCacheConfig.force |
Boolean | 为 true 时绕过网络响应缓存;默认值为 true |
fetchPolicy |
'network-only' | 'store-or-network' |
数据获取策略,默认 'network-only' |
其中 fetchPolicy 虽然在本版本文档正文中未展开,但源码 fetchQuery.js 明确支持两种取值:
network-only(默认):始终发起网络请求,无视 store 中是否已有可用数据;store-or-network:先调用environment.check(operation)检查查询可用性,若queryAvailability.status === 'available'则直接从 store 读取(environment.lookup(operation.fragment)),否则才走网络;同时会通过environment.__log记录fetchquery.fetch事件(含fetchPolicy、queryAvailability、shouldFetch字段)供诊断。
force 默认值 true 也在源码中得到印证——fetchQuery.js 中以 {force: true, ...options?.networkCacheConfig} 的方式合并配置,因此即使不传 options,网络响应缓存默认也被绕过。若需要命中响应缓存,可显式传入 {networkCacheConfig: {force: false}}。
Flow 类型参数 TQuery
fetchQuery<TQuery>(...) 中的 TQuery 是对应查询的 Flow 类型,可以从编译器自动生成的 <query_name>.graphql.js 文件中导入。它的作用有两方面:
- 保证 Observable 提供给
next回调的data形状与查询定义匹配; - 强制
variables输入与查询期望的变量类型一致。
类型定义见 fetchQuery.d.ts:
export function fetchQuery<T extends OperationType>(
environment: Environment,
taggedNode: GraphQLTaggedNode,
variables: T['variables'],
cacheConfig?: {
networkCacheConfig?: CacheConfig | null | undefined;
fetchPolicy?: FetchQueryFetchPolicy | null | undefined;
} | null,
): RelayObservable<T['response']>;
可以看到,底层返回的 RelayObservable 的泛型参数正是 T['response'],类型安全性由此贯穿整个链路。
返回值:Observable 详解
fetchQuery 返回一个 observable 实例。注意:调用 fetchQuery 本身并不会立即发起请求,只有调用 .subscribe() 或 .toPromise() 后请求才开始。
subscribe(observer)
订阅网络请求,返回一个 subscription 对象;调用 subscription.unsubscribe() 可以取消网络请求。observer 对象可包含以下事件处理函数:
| 事件 | 回调参数 | 触发时机 |
|---|---|---|
start |
subscription |
网络请求开始,参数代表对该网络 observable 的订阅 |
next |
data |
每次从网络收到 payload 时触发;data 是该时刻从 Relay store 读取的查询数据快照 |
complete |
无 | 网络请求成功完成 |
error |
error |
网络请求发生错误 |
unsubscribe |
subscription |
订阅被取消时触发 |
源码注释(fetchQuery.js)进一步阐明了这些事件的语义:
fetchQuery(environment, query, variables).subscribe({
// Called when network requests starts
start: (subscription) => {},
// Called after a payload is received and written to the local store
next: (payload) => {},
// Called when network requests errors
error: (error) => {},
// Called when network requests fully completes
complete: () => {},
// Called when network request is unsubscribed
unsubscribe: (subscription) => {},
});
特别值得注意 next 的语义:payload 收到后会先写入本地 store,再回调 next,因此 data 是从 store 读取的最新快照,而不是网络的原始响应。实现上,fetchQuery 内部通过 getNetworkObservable 将 fetchQueryInternal.fetchQuery 的网络 observable 与 environment.lookup(operation.fragment) 的 store 快照映射在一起(见 fetchQuery.js),并通过 handlePotentialSnapshotErrors 处理字段级错误。
此外,当启用 Relay 的增量数据投递(Incremental Data Delivery,参见 streaming-pagination)能力时,next 可能被调用多次以接收来自服务器的多个 payload。
toPromise()
将请求转换为 Promise:
- 返回值是一个 Promise,在收到服务器第一个网络响应时 resolve;
- 若请求失败则 reject;
- 不可取消。
关于 toPromise 的更多限制与风险,见下文专门章节。
三条核心行为
官方文档明确了 fetchQuery 的三个关键行为,理解它们对正确使用至关重要:
-
自动写入 store 并通知订阅者:
fetchQuery会把获取到的数据自动保存到内存中的 Relay store,并通知订阅了相关数据的组件重新渲染。 -
不保留(retain)查询数据:
fetchQuery不会 retain 查询数据,因此请求完成后数据不保证仍然保留在 store 中(可能被 Relay 的垃圾回收机制清理)。如果你希望数据在请求作用域之外继续存活,需要直接调用environment.retain()手动保留查询。详见 Controlling Relay's GC Policy。这一行为在测试中被显式验证:fetchQuery-test.js中"fetches request and does not retain data"用例断言请求完成后retained.length === 0(见 fetchQuery-test.js)。 -
自动去重(de-dupe):同一时刻、同一环境、同一查询与变量、且同样由
fetchQuery发起的**在途(in-flight)**请求会被自动去重,不会重复发起网络请求。
源码剖析:去重机制如何工作
去重的核心实现在 fetchQueryInternal.js。其机制可以概括为"环境级请求缓存 + 可重放的 ReplaySubject":
- 每个 environment 对应一个
Map<RequestIdentifier, RequestCacheEntry>请求缓存,整体存储在一个WeakMap(支持时)中(fetchQueryInternal.js); - 请求标识符
RequestIdentifier由查询与变量计算得出; - 首个订阅者触发真实网络请求,并把响应转发给一个
RelayReplaySubject;后续同标识符的订阅者直接订阅该subject,从而共享同一次网络请求; - 请求结束时(无论成功还是失败)通过
.finally(() => requestCache.delete(identifier))从缓存中移除条目(fetchQueryInternal.js); - 当某个订阅者取消时,如果该请求已无任何观察者(
subject.getObserverCount() === 0),底层网络请求订阅也会被一并取消并从缓存删除(fetchQueryInternal.js)。
边界情况:如果一个请求同步完成,那么在同一 tick 内再次以相同参数调用 fetchQuery 不会触发去重,因为该请求已不再处于在途状态(源码注释 fetchQuery.js 明确记录了这一行为)。
另外,fetchQueryInternal.js 还导出了 getPromiseForActiveRequest 与 getObservableForActiveRequest 等低层工具,供非 fetchQuery 场景复用同一套请求缓存。
慎用 .toPromise():数据可能缺失
toPromise 的便利背后有一个重要陷阱。官方文档原文给出警告:toPromise 会启动查询,并在第一份数据从服务器返回时 resolve,同时取消后续处理。这意味着查询中任何 deferred(延迟)字段或 3D 数据可能尚未被处理,从而造成数据缺失。因此官方明确建议一般情况下不要使用 toPromise()。
import type {AppQuery} from 'AppQuery.graphql';
const {fetchQuery} = require('react-relay');
fetchQuery<AppQuery>(
environment,
graphql`
query AppQuery($id: ID!) {
user(id: $id) {
name
}
}
`,
{id: 4},
)
.toPromise() // NOTE: don't use, this can cause data to be missing!
.then(data => {...})
.catch(error => {...});
配套的限制还包括:请求失败时 Promise 会 reject;Promise 一旦发出无法取消。如果确实需要 Promise 语义,请评估上述数据缺失与不可取消的代价是否可接受;否则推荐使用 subscribe 并配合 complete/unsubscribe 事件管理生命周期。
与测试用例的相互印证
仓库中的 fetchQuery-test.js 覆盖了本文涉及的大部分行为,可作为阅读与调试的参考:
- 不 retain 数据:
fetches request and does not retain data断言完成时retained.length === 0; - next 回调数据:
provides data snapshot on next断言next收到从 store 读取的快照{node: {id: '4'}}; - 错误处理:
handles error correctly断言error事件收到'Oops'错误; - 取消订阅:
unsubscribes when request is disposed验证unsubscribe事件; - toPromise 语义:
fetches request and does not retain query data验证 resolve 后数据、请求仍在途(isLoading为 true)直至手动 complete; - store-or-network 策略:
fetches data if not cached yet与reads from store if cached already分别验证"store 无数据走网络"与"store 有数据直接读"两条分支; - 字段级错误:
fetchQuery with missing @required value用例验证了缺失@required值时next仍会触发、并通过relayFieldLogger记录告警的行为。
总结与选型建议
一句话总结 fetchQuery 的使用要点:
- 在 React 之外做命令式查询,用它;在组件渲染中做数据获取,优先考虑
useLazyLoadQuery/useQueryLoader等 Hook; - 默认
force: true绕过网络响应缓存,默认fetchPolicy: 'network-only'永远走网络;需要读缓存时改用'store-or-network'; - 永远记得它不 retain 数据,需要长期保留时配合
environment.retain()(参考 availability-of-data); - 尽量使用
.subscribe()管理请求生命周期,避免toPromise()导致 deferred/3D 数据缺失。
更完整的函数级文档可继续阅读 fetchQuery.d.ts 与当前版本的 fetch-query 文档(新版新增了 fetchPolicy 与 networkCacheConfig 的完整说明)。