在 Relay 驱动的 React 应用中,"刷新查询"是指用完全相同的查询与变量,从服务器重新获取当前页面渲染所用的那部分数据,以获得数据的最新版本。本文基于本仓库 refreshing-queries.md 的完整讲解,结合react-relay中useQueryLoader、useLazyLoadQuery、fetchQuery、loadQuery的真实源码实现,系统地讲解三种刷新查询的写法、fetchPolicy与fetchKey的底层原理,以及如何避免刷新时出现 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传给使用usePreloadedQuery的MainContent,使其渲染更新后的数据; fetchPolicy: 'network-only':确保总是从网络获取、跳过本地数据缓存(四种fetchPolicy的详细语义见下文第五节);- 会触发 Suspense:调用
loadQuery会触发组件重渲染,而由于network-only必然发起网络请求,usePreloadedQuery会挂起(suspend)(原理见 Loading States with Suspense)。因此必须确保有一个Suspense边界包裹MainContent,以便展示 fallback 加载态。
3.2 从源码看loadQuery为什么"每次调用都会重新求值"
理解这一步的底层行为,可以直接阅读 loadQuery.js。其中有三个关键事实:
每次调用都自增
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 缓存结果,从而触发按需的重新拉取。默认
fetchPolicy是store-or-network。源码第 49 行定义了DEFAULT_FETCH_POLICY: FetchPolicy = 'store-or-network';这解释了为什么刷新时必须显式传入network-only——否则loadQuery会复用本地缓存,不会真正发出网络请求。网络缓存配置强制
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,其默认fetchPolicy是network-only(第 138 行),并且networkCacheConfig同样默认force: true;它还支持store-or-network:当 store 中数据不完整时才发网络请求,否则直接从 store 读取。
四、使用useLazyLoadQuery刷新查询
useLazyLoadQuery在渲染期间惰性获取数据(详见 use-lazy-load-query.md 与 Lazily Fetching Queries during Render)。因为没有queryRef可替换,刷新手段变为:更新fetchKey与fetchPolicy,触发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用新的fetchKey与fetchPolicy重渲染,进而在渲染时重新拉取查询; - 每次递增
fetchKey:为useLazyLoadQuery传入与上次渲染不同的fetchKey,会强制查询被完整重新求值并重新拉取,即使 variables 没有变化、组件也没有重新挂载; fetchPolicy: 'network-only':确保总是走网络、跳过本地缓存;- 会触发 Suspense:由于
network-only必然发起网络请求,刷新事件处理器中的 state 更新会让组件挂起,因此必须用Suspense边界包裹MainContent来展示 fallback。
4.2 从源码看fetchKey与 options
useLazyLoadQuery的 options 类型(useLazyLoadQuery.js)明确包含:
fetchPolicy:决定是否使用本地缓存、何时发网络请求(详见下节);fetchKey:string | number类型,"若与上一次渲染时不同,当前查询将针对 store 重新求值,并可能根据当前fetchPolicy与缓存状态重新拉取";networkCacheConfig:默认{force: true},用于绕过网络层的查询响应缓存。
在实现中(useLazyLoadQuery.js),它通过useMemoOperationDescriptor构造 operation descriptor,并把fetchKey、fetchPolicy与fetchQuery(environment, query)得到的 observable 一并交给useLazyLoadQueryNode处理——这就是"新fetchKey触发重新求值"的落点。同时usePreloadedQuery也是基于同一个useLazyLoadQueryNode实现的(usePreloadedQuery.js),它从preloadedQuery中取出fetchKey、fetchPolicy、source三个字段传给该节点,这也是 3.1 节中"新的queryRef会被独立求值"的原因。
4.3 如果不想触发 Suspense 回退
与 3.4 节思路一致:先fetchQuery把数据写入 store,完成后再通过 state 更新fetchKey与store-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,携带递增的fetchKey与store-only,使useLazyLoadQuery重渲染时只读取已缓存的更新数据; store-only避免挂起:state 更新时数据已在 store 中,因此不发网络请求、不挂起。
五、核心机制深挖:fetchPolicy、fetchKey与数据保留
5.1 四种fetchPolicy的完整语义
本文两个刷新场景反复用到network-only与store-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 | 触发方式 | 是否挂起 | 关键配置 |
|---|---|---|---|---|
再次loadQuery | useQueryLoader+usePreloadedQuery | 事件处理器 | 是 | fetchPolicy: 'network-only' |
fetchQuery+loadQuery(store-only) | 同上 | 事件处理器 | 否 | 先network-only写缓存,再store-only读缓存 |
递增fetchKey | useLazyLoadQuery | state 更新 | 是 | 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,且loadQuery与useQueryLoader返回的loadQuery回调不能在 React 渲染阶段调用(否则会抛错)。按照上述方案,你就能在保持 Suspense 优雅加载态与"不打断已渲染内容"之间做出合适的取舍。
- 前端
- 开发工具
【免费下载链接】relay
Relay is a JavaScript framework for building>项目地址:https://gitcode.com/gh_mirrors/relay29/relay
相关推荐
Relay 查询刷新(Refreshing Queries)完全指南:从 `useQueryLoader`、`useLazyLoadQuery` 到 `fetchQuery` 的实战方案
Relay 查询刷新(Refreshing Queries)完全指南:从 useQueryLoader 、 useLazyLoadQuery 到 fetchQu
前端开发工具Relay 查询刷新(Refreshing Queries)实战指南:基于 `useQueryLoader`、`useLazyLoadQuery` 与 `fetchQuery` 的完整实现
Relay 查询刷新(Refreshing Queries)实战指南:基于 useQueryLoader 、 useLazyLoadQuery 与 fetchQ
前端开发工具Relay 查询刷新(Refreshing Queries)实战指南:用 useQueryLoader 与 useLazyLoadQuery 拉取最新数据
Relay 查询刷新(Refreshing Queries)实战指南:用 useQueryLoader 与 useLazyLoadQuery 拉取最新数据 本篇
前端开发工具