news 2026/9/22 18:36:24

Relay loadQuery 完整指南:用命令式预加载实现 render-as-you-fetch 数据获取

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Relay loadQuery 完整指南:用命令式预加载实现 render-as-you-fetch 数据获取

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(), // 本例为了简洁省略了这一步。

这个示例包含了三个要点:

  1. 事件驱动调用loadQuery应该在事件回调中调用,而不是在组件渲染顶层执行,否则会违反 React 渲染阶段的规则(详见下文"行为"一节)。
  2. 获取到的 query reference 交给usePreloadedQuery()消费:渲染层通过usePreloadedQuery(query, queryReference)从 Relay store 读取数据,查询进行中时会触发 Suspense 挂起,失败时抛出错误,成功时返回查询结果(完整的消费示例见 use-preloaded-query.md)。
  3. 手动释放: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

类型:GraphQLTaggedNodegraphql模板字面量)或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'决定是否复用本地缓存数据,以及是否发送网络请求
networkCacheConfigCacheConfig{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预加载的网络请求默认总是穿透网络层响应缓存,避免复用过期响应;你仍可通过该选项传入其他网络层配置项(如pollmetadata等)来定制行为。

environmentProviderOptions(可选)

类型:TEnvironmentProviderOptions。传递给environmentProvider的选项对象,用于 EntryPoint 体系中的prepareSurfaceEntryPoint.js场景——即由环境提供方决定最终使用哪个环境执行请求。

Flow 类型参数

  • TQuery:应与指定查询的 Flow 类型对应。该类型可从自动生成的文件<query_name>.graphql.js中导入。
  • TEnvironmentProviderOptionsenvironmentProviderOptions参数的类型。

从类型定义看(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),返回的retainReferencereleaseQuery中被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的完整执行链路,这有助于深入理解其行为:

  1. 生成新fetchKey,保证引用的独立求值。
  2. 解析 fetchPolicy:若未显式传入,会根据请求类型推断默认值——live 查询或启用执行期 resolver 的查询默认使用'store-and-network',纯客户端查询(无服务端操作可拉取)使用'store-only',其余使用'store-or-network'(loadQuery.js)。
  3. 构造 operation 并保留:通过createOperationDescriptor创建操作描述符,随后environment.retain(operation)保留该操作对应的数据。
  4. 检查缓存可用性:若 fetchPolicy 允许命中缓存,则调用environment.check(operation);仅当状态非'available'(即缓存缺失)时才执行获取(loadQuery.js)。这正是'store-or-network'语义的落地实现。
  5. 两层去重
    • 网络层使用fetchQueryDeduped(environment, 'raw-network-request-' + identifier, ...)原始网络请求去重——保证同一 (environment, identifier) 对同时只有一个活跃请求;当查询 AST 尚未就绪时,多次调用仍可共用同一个网络请求(loadQuery.js)。
    • 操作执行层再次使用fetchQueryDeduped操作执行去重,并为 Suspense 基础设施跟踪活跃操作状态,避免对同一响应重复处理(loadQuery.js)。
  6. 急切执行 + ReplaySubject 重放:与通常惰性的 Observable 不同,loadQuery希望在调用时立即启动请求。实现上用ReplaySubject捕获急切执行期间发生的事件,再通过返回的 Observable 将事件重放给订阅者(loadQuery.js)。这也是usePreloadedQuery能拿到source并复用网络事件、避免二次请求的关键。
  7. 预加载查询的模块注册:当传入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

点击查看免费下载
上一篇:V3 Admin Vite:面向企业级应用的现代化Vue3中台架构设计
下一篇:Basaran与ChatGLM-6B集成教程:构建中文对话AI的完整流程

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

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

PlatformIO 新建工程卡到报错?让 Codex 走 TaoToken 对照 Python 环境变量

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/22 18:10:17

Linux中断子系统移植指南:从irq_chip到irq_domain的适配与调试

1. 中断子系统到底在解决什么问题做Linux驱动移植的人&#xff0c;早晚都会撞上中断这块硬骨头。你从一颗芯片换到另一颗芯片&#xff0c;GPIO、时钟、引脚复用这些改改寄存器还能对付&#xff0c;但一旦涉及中断控制器换了型号&#xff0c;或者从ARM Cortex-A切到RISC-V&#…

作者头像 李华
网站建设 2026/9/22 18:53:03

前端AI编程工具横向对比:从选型框架到工作流实战

前端圈子这两年最明显的变化&#xff0c;不是某个框架又出了新版本&#xff0c;而是写代码的方式本身在变。以前我们讨论的是用 Vite 还是 Webpack、用 Pinia 还是 Redux&#xff0c;现在群里聊得最多的是"你那个 AI 编程工具续费了没""哪个补全更懂我的组件库&…

作者头像 李华
网站建设 2026/9/21 17:58:13

一条蛇引发的旧情复燃:分手五年后如何打破沉默

1. 一条深夜消息把五年拉回到同一个瞬间那天晚上我刚关灯&#xff0c;手机屏幕突然亮了。一条微信消息&#xff0c;没有任何铺垫&#xff0c;只有一句话&#xff1a;“你那里有条蛇&#xff01;”发消息的人&#xff0c;我五年没联系了。准确说&#xff0c;不是没联系&#xff…

作者头像 李华
网站建设 2026/9/21 17:57:43

二维网格回溯算法实战:从单词搜索到数独求解

1. 回溯算法在二维网格中的实战应用回溯算法在二维网格问题中展现出独特的解题魅力。这类问题通常需要在网格上进行路径搜索、区域划分或模式匹配&#xff0c;而回溯提供了一种系统性的试错方法。我们来看一个经典案例&#xff1a;单词搜索问题。给定一个mn的二维字符网格和一个…

作者头像 李华