news 2026/9/24 17:21:37

Relay 20 刷新查询(Refreshing Queries)实战指南:useQueryLoader、useLazyLoadQuery 与 fetchQuery 三种刷新方案详解

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Relay 20 刷新查询(Refreshing Queries)实战指南:useQueryLoader、useLazyLoadQuery 与 fetchQuery 三种刷新方案详解

在 Relay 驱动的 React 应用中,"刷新查询"是指用完全相同的查询与变量,从服务器重新获取当前页面渲染所用的那部分数据,以获得数据的最新版本。本文基于本仓库 refreshing-queries.md 的完整讲解,结合react-relayuseQueryLoaderuseLazyLoadQueryfetchQueryloadQuery的真实源码实现,系统地讲解三种刷新查询的写法、fetchPolicyfetchKey的底层原理,以及如何避免刷新时出现 Suspense 回退。读完本文,你将能够在自己的 Relay 应用中按需实现"手动刷新最新数据"的完整方案,并理解每条代码背后的数据流与缓存机制。

一、什么是"刷新查询",它与"重取不同数据"有何区别

在 refreshing-queries.md 的开篇,Relay 对术语做了严格定义:

当我们说"refreshing a query"(刷新查询),指的是获取查询原本渲染的完全相同的数据,目的是从服务器拿到这份数据最新的版本。

与之相对的是"使用不同数据重新查询"(refetching queries with different data),后者指改变查询变量以渲染不同的内容,例如切换当前选中项、渲染另一组列表,其详细方案见 refetching-queries-with-different-data.md。

两者在实现上的关键差异是:

  • 刷新:保持 variables 不变,强制绕过本地缓存从网络重新拉取(或配合fetchQuery先写缓存再读缓存);
  • 重取不同数据:传入不同的 variables,让查询按新变量重新求值。

二、先考虑实时特性:刷新不是唯一选择

在动手实现"定时/手动刷新"之前,文档提醒我们:如果希望数据始终与服务器保持同步,首先应该评估是否适合使用实时特性,让数据自动保持最新,而无需周期性手动刷新。

一个典型例子是使用GraphQL Subscriptions(订阅),详见 graphql-subscriptions.md。订阅需要在你的 GraphQL 服务器端以及 网络层(network layer) 上做额外配置。只有当实时方案不合适(例如服务器不支持订阅、或数据变更频率低、成本高)时,才采用下文的手动刷新方案。

三、使用useQueryLoader/loadQuery刷新查询

useQueryLoader是"render-as-you-fetch"模式的核心 Hook,负责在事件处理器中立即发起网络请求,并持有queryRef(一个PreloadedQuery实例)。其详细 API 见 use-query-loader.md,基本用法见 Fetching Queries for Render 一节。

3.1 最简单的刷新:再次调用loadQuery

文档给出的核心结论是:刷新只需要用相同的 variables 再次调用loadQuery。完整示例(App.react.js):

/** * App.react.js */ const AppQuery = require('__generated__/AppQuery.graphql'); function App(props: Props) { const [queryRef, loadQuery] = useQueryLoader( AppQuery, props.appQueryRef /* initial query ref */ ); const refresh = useCallback(() => { // Load the query again using the same original variables. // Calling loadQuery will update the value of queryRef. // The fetchPolicy ensures we always fetch from the server and skip // the local data cache. const {variables} = props.appQueryRef; loadQuery(variables, {fetchPolicy: 'network-only'}); }, [/* ... */]); return ( <React.Suspense fallback="Loading query..."> <MainContent refresh={refresh} queryRef={queryRef} /> </React.Suspense> ); }

配套的渲染组件(MainContent.react.js)通过usePreloadedQuery读取数据:

/** * MainContent.react.js */ // Renders the preloaded query, given the query reference function MainContent(props) { const {refresh, queryRef} = props; const data = usePreloadedQuery( graphql` query AppQuery($id: ID!) { user(id: $id) { name friends { count } } } `, queryRef, ); return ( <> <h1>{data.user?.name}</h1> <div>Friends count: {data.user.friends?.count}</div> <Button onClick={() => refresh()}> Fetch latest count </Button> </> ); }

让我们拆解这段代码的要点:

  • 在事件处理器中调用loadQuery:网络请求立即开始,随后将更新后的queryRef传给使用usePreloadedQueryMainContent,使其渲染更新后的数据;
  • fetchPolicy: 'network-only':确保总是从网络获取、跳过本地数据缓存(四种fetchPolicy的详细语义见下文第五节);
  • 会触发 Suspense:调用loadQuery会触发组件重渲染,而由于network-only必然发起网络请求,usePreloadedQuery挂起(suspend)(原理见 Loading States with Suspense)。因此必须确保有一个Suspense边界包裹MainContent,以便展示 fallback 加载态。

3.2 从源码看loadQuery为什么"每次调用都会重新求值"

理解这一步的底层行为,可以直接阅读 loadQuery.js。其中有三个关键事实:

  1. 每次调用都自增fetchKey。源码第 100–109 行的注释明确写道:

    "Every time you call loadQuery, we will generate a new fetchKey. This will ensure that every query reference that is created and passed to usePreloadedQuery is independently evaluated, even if they are for the same query/variables."

    即 loadQuery.js 中fetchKey++保证:即使查询与变量完全相同,新的queryRef也会被独立求值,usePreloadedQuery不会复用旧的 Suspense 缓存结果,从而触发按需的重新拉取。

  2. 默认fetchPolicystore-or-network。源码第 49 行定义了DEFAULT_FETCH_POLICY: FetchPolicy = 'store-or-network';这解释了为什么刷新时必须显式传入network-only——否则loadQuery会复用本地缓存,不会真正发出网络请求。

  3. 网络缓存配置强制force: true。源码第 115–118 行将networkCacheConfig{force: true}合并,绕过网络层的响应缓存,保证请求真正到达服务器。

3.3 配合useQueryLoader的刷新行为

useQueryLoader内部(见 useQueryLoader.js)会把loadQuery的返回结果保存在 state 中并更新queryRef,同时把旧引用登记到"未释放引用集合",在提交新引用后统一释放旧的queryRef,避免 Relay store 中的数据泄漏。这意味着你反复调用loadQuery刷新时,内存中的旧查询数据会被妥善清理。

3.4 如果不想让刷新触发 Suspense 回退

刷新时network-only必然导致MainContent挂起,此时Suspense fallback 会隐藏已经渲染好的内容。如果不想让用户看到内容被替换为加载态,文档推荐改用fetchQuery并自行维护加载状态:

/** * App.react.js */ const AppQuery = require('__generated__/AppQuery.graphql'); function App(props: Props) { const environment = useRelayEnvironment(); const [queryRef, loadQuery] = useQueryLoader( AppQuery, props.appQueryRef /* initial query ref */ ); const [isRefreshing, setIsRefreshing] = useState(false) const refresh = useCallback(() => { if (isRefreshing) { return; } const {variables} = props.appQueryRef; setIsRefreshing(true); // fetchQuery will fetch the query and write // the data to the Relay store. This will ensure // that when we re-render, the data is already // cached and we don't suspend fetchQuery(environment, AppQuery, variables) .subscribe({ complete: () => { setIsRefreshing(false); // *After* the query has been fetched, we call // loadQuery again to re-render with a new // queryRef. // At this point the data for the query should // be cached, so we use the 'store-only' // fetchPolicy to avoid suspending. loadQuery(variables, {fetchPolicy: 'store-only'}); } error: () => { setIsRefreshing(false); } }); }, [/* ... */]); return ( <React.Suspense fallback="Loading query..."> <MainContent isRefreshing={isRefreshing} refresh={refresh} queryRef={queryRef} /> </React.Suspense> ); }

拆解要点:

  • 自行维护isRefreshing状态:因为我们避免挂起,需要用这个状态在MainContent内部渲染一个忙碌指示器(spinner)之类的 UI,而不会隐藏MainContent本身
  • fetchQuery写缓存:事件处理器中先调用fetchQuery,它会发起请求并把响应写入本地 Relay store;待网络请求完成后再调用loadQuery,得到更新后的queryRef传给usePreloadedQuery渲染新数据;
  • store-only避免挂起:此时查询数据已在本地 store 中缓存,因此用fetchPolicy: 'store-only'只读取缓存,不发起网络请求、不挂起。

fetchQuery的 API 细节(包括其 Observable 的complete/error/next事件)见 fetch-query.md。从源码看(fetchQuery.js),fetchQuery会创建 operation descriptor,其默认fetchPolicynetwork-only(第 138 行),并且networkCacheConfig同样默认force: true;它还支持store-or-network:当 store 中数据不完整时才发网络请求,否则直接从 store 读取。

四、使用useLazyLoadQuery刷新查询

useLazyLoadQuery渲染期间惰性获取数据(详见 use-lazy-load-query.md 与 Lazily Fetching Queries during Render)。因为没有queryRef可替换,刷新手段变为:更新fetchKeyfetchPolicy,触发useLazyLoadQuery重新求值并重新拉取

4.1 基本方案:递增fetchKey+network-only

/** * App.react.js */ const AppQuery = require('__generated__/AppQuery.graphql'); function App(props: Props) { const variables = {id: '4'}; const [refreshedQueryOptions, setRefreshedQueryOptions] = useState(null); const refresh = useCallback(() => { // Trigger a re-render of useLazyLoadQuery with the same variables, // but an updated fetchKey and fetchPolicy. // The new fetchKey will ensure that the query is fully // re-evaluated and refetched. // The fetchPolicy ensures that we always fetch from the network // and skip the local data cache. setRefreshedQueryOptions(prev => ({ fetchKey: (prev?.fetchKey ?? 0) + 1, fetchPolicy: 'network-only', })); }, [/* ... */]); return ( <React.Suspense fallback="Loading query..."> <MainContent refresh={refresh} queryOptions={refreshedQueryOptions ?? {}} variables={variables} /> </React.Suspense> ); }
/** * MainContent.react.js */ // Fetches and renders the query, given the fetch options function MainContent(props) { const {refresh, queryOptions, variables} = props; const data = useLazyLoadQuery( graphql` query AppQuery($id: ID!) { user(id: $id) { name friends { count } } } `, variables, queryOptions, ); return ( <> <h1>{data.user?.name}</h1> <div>Friends count: {data.user.friends?.count}</div> <Button onClick={() => refresh()}> Fetch latest count </Button> </> ); }

拆解要点:

  • 用 state 保存新的查询选项:刷新事件处理器中更新 state,令MainContent用新的fetchKeyfetchPolicy重渲染,进而在渲染时重新拉取查询;
  • 每次递增fetchKey:为useLazyLoadQuery传入与上次渲染不同的fetchKey,会强制查询被完整重新求值并重新拉取,即使 variables 没有变化、组件也没有重新挂载;
  • fetchPolicy: 'network-only':确保总是走网络、跳过本地缓存;
  • 会触发 Suspense:由于network-only必然发起网络请求,刷新事件处理器中的 state 更新会让组件挂起,因此必须用Suspense边界包裹MainContent来展示 fallback。

4.2 从源码看fetchKey与 options

useLazyLoadQuery的 options 类型(useLazyLoadQuery.js)明确包含:

  • fetchPolicy:决定是否使用本地缓存、何时发网络请求(详见下节);
  • fetchKeystring | number类型,"若与上一次渲染时不同,当前查询将针对 store 重新求值,并可能根据当前fetchPolicy与缓存状态重新拉取";
  • networkCacheConfig:默认{force: true},用于绕过网络层的查询响应缓存。

在实现中(useLazyLoadQuery.js),它通过useMemoOperationDescriptor构造 operation descriptor,并把fetchKeyfetchPolicyfetchQuery(environment, query)得到的 observable 一并交给useLazyLoadQueryNode处理——这就是"新fetchKey触发重新求值"的落点。同时usePreloadedQuery也是基于同一个useLazyLoadQueryNode实现的(usePreloadedQuery.js),它从preloadedQuery中取出fetchKeyfetchPolicysource三个字段传给该节点,这也是 3.1 节中"新的queryRef会被独立求值"的原因。

4.3 如果不想触发 Suspense 回退

与 3.4 节思路一致:先fetchQuery把数据写入 store,完成后再通过 state 更新fetchKeystore-only,让重渲染时只读缓存、不挂起:

/** * App.react.js */ import type {AppQuery as AppQueryType} from 'AppQuery.graphql'; const AppQuery = require('__generated__/AppQuery.graphql'); function App(props: Props) { const variables = {id: '4'} const environment = useRelayEnvironment(); const [refreshedQueryOptions, setRefreshedQueryOptions] = useState(null); const [isRefreshing, setIsRefreshing] = useState(false) const refresh = useCallback(() => { if (isRefreshing) { return; } setIsRefreshing(true); // fetchQuery will fetch the query and write // the data to the Relay store. This will ensure // that when we re-render, the data is already // cached and we don't suspend fetchQuery(environment, AppQuery, variables) .subscribe({ complete: () => { setIsRefreshing(false); // *After* the query has been fetched, we update // our state to re-render with the new fetchKey // and fetchPolicy. // At this point the data for the query should // be cached, so we use the 'store-only' // fetchPolicy to avoid suspending. setRefreshedQueryOptions(prev => ({ fetchKey: (prev?.fetchKey ?? 0) + 1, fetchPolicy: 'store-only', })); } error: () => { setIsRefreshing(false); } }); }, [/* ... */]); return ( <React.Suspense fallback="Loading query..."> <MainContent isRefreshing={isRefreshing} refresh={refresh} queryOptions={refreshedQueryOptions ?? {}} variables={variables} /> </React.Suspense> ); }

拆解要点:

  • 自行维护isRefreshing:在MainContent内部渲染 busy spinner 等加载 UI,不隐藏已渲染的内容;
  • fetchQuery写 store:请求完成后再更新 state,携带递增的fetchKeystore-only,使useLazyLoadQuery重渲染时只读取已缓存的更新数据;
  • store-only避免挂起:state 更新时数据已在 store 中,因此不发网络请求、不挂起。

五、核心机制深挖:fetchPolicyfetchKey与数据保留

5.1 四种fetchPolicy的完整语义

本文两个刷新场景反复用到network-onlystore-only,这里给出四种取值在 Relay 20 中的完整语义(与 fetch-policies.md 及useLazyLoadQuery源码注释一致):

fetchPolicy是否复用本地缓存是否发网络请求适用场景
store-or-network(默认)复用仅当查询存在缺失或过期数据时常规渲染、首次加载
store-and-network复用总是发送(无论缓存是否完整)需要立即渲染缓存并后台更新
network-only不复用总是发送强制刷新最新数据
store-only仅读缓存从不发送配合fetchQuery写缓存后的"只读"刷新

需要特别注意:loadQuery的默认策略是store-or-network(见 loadQuery.js),所以刷新时必须显式传network-only;而在先fetchQuery写缓存后,改用store-only才能避免挂起。此外useLazyLoadQuery的 options 中networkCacheConfig默认值为{force: true}(useLazyLoadQuery.js),loadQuery也强制合并force: true(loadQuery.js),用于绕过网络层的响应缓存。

5.2fetchKey:强制重新求值的"查询版本号"

fetchKey的作用可以类比 React 组件上的key:给它传一个与上次渲染不同的值,即使查询与 variables 都未改变、组件也未重新挂载,当前查询也会被重新求值(re-evaluate),并依据fetchPolicy与缓存状态决定是否重新拉取。

  • useQueryLoader场景下,loadQuery每次调用都会自增一个内部fetchKey(loadQuery.js),因此每次刷新都会产生"新版本"的queryRef
  • useLazyLoadQuery场景下,fetchKey由调用方传入 options,刷新时手动递增(如(prev?.fetchKey ?? 0) + 1)即可达到同样效果。

5.3fetchQuery的写入、去重与保留行为

从 fetch-query.md 的 Behavior 一节和源码可以确认:

  • fetchQuery会自动把拉取的数据写入内存中的 Relay store,并通知订阅了相关数据的组件;
  • fetchQuery不会 retain(保留)查询数据——请求完成后数据随时可能被垃圾回收;若需要保证数据在请求结束后仍然留在 store 中,必须调用environment.retain()
  • fetchQuery会对相同查询与变量且同时在途的请求做自动去重(dedupe);
  • 建议用.subscribe(...)而非.toPromise()toPromise()在收到第一份数据后就取消后续处理,可能丢失 deferred / 3D 等增量数据。

这正是 3.4 / 4.3 节"避免 Suspense"方案成立的前提:fetchQuery完成写入后,store 中已有完整数据,此时store-only才能安全地只读缓存。

六、方案对比与选择建议

刷新方案适用 Hook触发方式是否挂起关键配置
再次loadQueryuseQueryLoader+usePreloadedQuery事件处理器fetchPolicy: 'network-only'
fetchQuery+loadQuery(store-only)同上事件处理器network-only写缓存,再store-only读缓存
递增fetchKeyuseLazyLoadQuerystate 更新fetchKey递增 +fetchPolicy: 'network-only'
fetchQuery+ state 更新store-only同上state 更新先写缓存,再更新fetchKey+store-only

选择建议:

  • 默认优先使用"render-as-you-fetch"useQueryLoader/usePreloadedQuery组合:loadQuery会在事件处理器中立即发请求,性能更优,且useQueryLoader会自动释放旧的queryRef,避免 store 数据泄漏(详见 load-query.md 与 use-preloaded-query.md);
  • 如果刷新时不希望已有内容被 fallback 隐藏(例如列表页下拉刷新),就采用fetchQuery+store-only的两段式方案,同时自行维护isRefreshing状态来显示轻量加载指示;
  • useLazyLoadQuery刷新适合简单场景,但如文档与源码注释(useLazyLoadQuery.js)所指出的,它把数据获取推迟到渲染期,容易造成嵌套或瀑布式往返(waterfall),应谨慎使用。

最后提醒:所有刷新方案都要求应用根部挂载 RelayEnvironmentProvider 提供 Relay environment,且loadQueryuseQueryLoader返回的loadQuery回调不能在 React 渲染阶段调用(否则会抛错)。按照上述方案,你就能在保持 Suspense 优雅加载态与"不打断已渲染内容"之间做出合适的取舍。

  • 前端
  • 开发工具

【免费下载链接】relay

Relay is a JavaScript framework for building>项目地址:https://gitcode.com/gh_mirrors/relay29/relay

点击查看免费下载
上一篇:Beekeeper Studio 支持的数据库全景:支持矩阵、版本差异与源码级连接机制解析
下一篇:TDengine 零代码数据接入:通过 taosExplorer 将 MySQL 数据迁移/同步至 TDengine 完整指南

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

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

冲锋衣内衬北极绒怎么选?154g/㎡单面绒参数解析与采购要点

冲锋衣内衬选用北极绒&#xff0c;不能只比较柔软度、克重和单价。更有效的选料方法是&#xff1a;先明确使用部位和成衣结构&#xff0c;再核对面料规格&#xff0c;通过样布与样衣验证&#xff0c;最后确定大货验收和供应条件。本文以154g/㎡经编单面北极绒为例&#xff0c;梳…

作者头像 李华
网站建设 2026/9/24 17:14:52

Ubuntu20.04安装Vulkan

Ubuntu20.04安装Vulkan Vulkan是由科纳斯组织&#xff08;Khronos Group&#xff09;主导开发的跨平台、低开销的图形与计算应用程序接口&#xff08;API&#xff09;。它旨在为开发者提供更直接、更精细的GPU&#xff08;图形处理器&#xff09;控制能力&#xff0c;以充分发…

作者头像 李华
网站建设 2026/9/24 17:12:59

pint:Python物理单位计算系统

文章目录简介单位和物理量*格式化输出简介 pint是Python的物理单位计算系统&#xff0c;通过指定量纲&#xff0c;避免因单位混淆导致的计算错误。支持pip和conda安装。 pip install pint -i https://pypi.tuna.tsinghua.edu.cn/simple conda install -c conda-forge pint单…

作者头像 李华