Relay 17 指南:使用不同的数据重新获取查询(useQueryLoader / loadQuery 与 useLazyLoadQuery 完整实战)
【免费下载链接】relayRelay is a JavaScript framework for building>项目地址: https://gitcode.com/gh_mirrors/relay29/relay
导读
本文聚焦 Relay 数据驱动 React 应用中的一个高频交互场景:"用不同的数据重新获取查询"(Refetching Queries with Different Data)——即在同一查询的基础上,通过改变查询变量(query variables)来切换当前展示的内容,例如切换当前选中的条目、渲染不同的列表,或在用户交互后过渡到新内容。文章完整覆盖useQueryLoader/loadQuery与useLazyLoadQuery两条实现路径,并深入讲解默认fetchPolicy、fetchKey语义、以及两种"避免 Suspense 隐藏已渲染内容"的实操方案。阅读本文后,你将能在 Relay 17(本仓库版本化文档 v17.0.0)项目中熟练实现带切换语义的查询重取,并理解其背后的源码机制。
什么是"用不同数据重新获取查询"
当我们在 Relay 中谈论"refetching a query"(重新获取查询)时,指的是为与最初渲染时不同的数据再次获取该查询。典型场景包括:
- 改变当前选中的条目(例如切换详情页展示的用户);
- 渲染与当前列表不同的另一组列表项;
- 更一般地,让当前渲染的内容过渡到新的或不同的内容。
这与 Refreshing Queries(刷新查询) 有本质区别:刷新是用完全相同的变量重新获取同一份数据,以获得服务端最新版本;而"用不同数据重新获取"则通过更换变量驱动内容切换。两者在实现上的差异仅在于调用loadQuery或更新 state 时所传入的 variables 是否变化,代码骨架高度相似,可相互对照学习。
Relay 提供了两条主流实现路径,分别对应两种数据获取模型:
- 预取(preload)模型:
useQueryLoader+loadQuery+usePreloadedQuery,参见 Fetching Queries for Render; - 渲染期获取(fetch-on-render)模型:
useLazyLoadQuery,参见 Lazily Fetching Queries during Render。
下面分别展开。
方式一:使用useQueryLoader/loadQuery重新获取
基础用法:传入不同的查询变量
与 Refreshing Queries with useQueryLoader 类似,我们使用useQueryLoaderHook,但这一次传入的是不同的查询变量:
/** * App.react.js */ const AppQuery = require('__generated__/AppQuery.graphql'); function App(props: Props) { const variables = {id: '4'}; const [queryRef, loadQuery] = useQueryLoader( AppQuery, props.appQueryRef /* initial query ref */ ); const refetch = useCallback(() => { // Load the query again using the same original variables. // Calling loadQuery will update the value of queryRef. loadQuery({id: 'different-id'}); }, [/* ... */]); return ( <React.Suspense fallback="Loading query..."> <MainContent refetch={refetch} queryRef={queryRef} /> </React.Suspense> ); }/** * MainContent.react.js */ // Renders the preloaded query, given the query reference function MainContent(props) { const {refetch, 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={() => refetch()}> Fetch latest count </Button> </> ); }要点解析:
- 我们在refetch 事件处理器内调用
loadQuery({id: 'different-id'}),因此网络请求会立即开始(而非等到渲染时);随后把更新后的queryRef传给usePreloadedQuery,使其渲染更新后的数据。 - 我们没有给
loadQuery传fetchPolicy,因此它使用默认值'store-or-network':优先复用本地 Relay store 中已有的缓存数据,仅当查询所依赖的数据缺失时才发起网络请求。你也可以显式传入其他策略来控制是否使用本地缓存,详见 Reusing Cached Data For Render(重用缓存数据渲染)。 - 调用
loadQuery会重新渲染组件,并可能导致usePreloadedQuery挂起(suspend),相关机制见 Loading States with Suspense。因此必须在包裹MainContent的位置提供Suspense边界,以便在挂起期间显示 fallback 加载态。
loadQuery 底层的 fetchKey 与默认 fetchPolicy
为什么"再调一次loadQuery"就能让usePreloadedQuery重新渲染新数据,而不是命中 React 层的缓存?答案在 packages/react-relay/relay-hooks/loadQuery.js 的实现中:
- 每次调用都会生成新的
fetchKey:源码第 66 行声明模块级计数器let fetchKey = 100001;,第 109 行在每次loadQuery时执行fetchKey++,并作为PreloadedQuery的一部分(第 415 行fetchKey)返回。源码注释明确指出:这样做的目的是确保每个新生成的 query ref 都被独立求值,避免出现"第二次调用loadQuery想触发 refetch,却被 Suspense 缓存复用了旧结果"的问题。 - 默认
fetchPolicy是'store-or-network':源码第 49 行const DEFAULT_FETCH_POLICY: FetchPolicy = 'store-or-network';。当未显式提供时(第 111-114 行),使用该默认值;对于带metadata.live的 live query 或启用了执行期 resolver 的查询,默认值则会变为'store-and-network'(第 50、52-64 行)。 store-only时直接跳过网络层:源码第 285-288 行,创建 operation descriptor 后先environment.retain(operation)保留数据防止被 GC;若fetchPolicy === 'store-only'则直接return,不发任何网络请求。store-or-network会先做可用性检查:第 293-297 行,仅当environment.check(operation).status !== 'available'时才真正发起网络请求;若 store 中已有完整可用数据,则跳过网络,直接读缓存。- 请求是 eager(立即执行)的:第 147-150 行使用
ReplaySubject捕获 eager 执行期间的事件,再通过 Observable 回放给订阅者,确保调用loadQuery的当下网络请求就已启动。
这解释了文档中"调用loadQuery会立即发起请求、并可能触发 Suspense"的行为根源。
如果希望避免 Suspense:fetchQuery先行
有些场景下,你不希望展示 Suspense fallback——因为 fallback 会隐藏已经渲染出来的内容。此时可以改用fetchQuery,并手动维护一个 loading 状态:
/** * 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 [isRefetching, setIsRefetching] = useState(false) const refetch = useCallback(() => { if (isRefetching) { return; } setIsRefetching(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: () => { setIsRefetching(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({id: 'different-id'}, {fetchPolicy: 'store-only'}); }, error: () => { setIsRefetching(false); } }); }, [/* ... */]); return ( <React.Suspense fallback="Loading query..."> <MainContent isRefetching={isRefetching} refetch={refetch} queryRef={queryRef} /> </React.Suspense> ); }注:上述代码中的
variables应替换为本次要获取的新变量(例如{id: 'different-id'}),与loadQuery中传入的变量保持一致。
要点解析:
- 既然不挂起,就要自行跟踪
isRefetchingloading 状态,用它来渲染忙碌 spinner 等加载 UI——而且是在MainContent内部渲染,从而不会隐藏MainContent本体。 - 事件处理器中先调用
fetchQuery:它负责从网络获取查询并把数据写入本地 Relay store。当fetchQuery的请求完成后,再调用loadQuery获得更新的queryRef,传给usePreloadedQuery渲染更新后的数据——整体思路与前一个示例一致,只是时序被拆成了两步。 - 此时再调用
loadQuery时,查询数据已经缓存在本地 store 中,因此使用fetchPolicy: 'store-only'避免挂起,仅从缓存中读取数据。
fetchQuery在这里承担关键角色,其语义可以从源码与 API 文档确认:
- 位于 packages/relay-runtime/query/fetchQuery.js,其默认
fetchPolicy为'network-only'(第 138 行options?.fetchPolicy ?? 'network-only'),即默认总是从网络获取; - 它会自动把获取到的数据写入内存 Relay store,并通知订阅了相关数据的组件(参见 fetchQuery API 参考 的 Behavior 一节);
- 它不会 retain(保留)数据,请求完成后数据不保证留在 store 中;如需保留需自行调用
environment.retain(); - 它会自动对相同查询 + 相同变量且同时在途的请求做去重。
另外注意:上面的例子仍保留了外层的Suspense边界,但正常情况下重取期间usePreloadedQuery不会挂起(数据已在缓存中),因此MainContent不会被 fallback 替换,而isRefetching状态则驱动了细粒度的加载反馈。若你使用的是 React 并发渲染的未来版本,React 将提供原生选项避免用 Suspense fallback 隐藏已渲染内容(详见本文对应的 OssAvoidSuspenseNote 说明)。
方式二:使用useLazyLoadQuery重新获取
基础用法:更新 variables 与 fetchKey
与 Refreshing Queries with useLazyLoadQuery 类似,我们也可以使用useLazyLoadQuery,区别在于这次传入不同的查询变量:
/** * App.react.js */ const AppQuery = require('__generated__/AppQuery.graphql'); function App(props: Props) { const [queryArgs, setQueryArgs] = useState({ options: {fetchKey: 0}, variables: {id: '4'}, }); const refetch = useCallback(() => { // Trigger a re-render of useLazyLoadQuery with new variables, // *and* an updated fetchKey. // The new fetchKey will ensure that the query is fully // re-evaluated and refetched. setQueryArgs(prev => ({ options: { fetchKey: (prev?.options.fetchKey ?? 0) + 1, }, variables: {id: 'different-id'} })); }, [/* ... */]); return ( <React.Suspense fallback="Loading query..."> <MainContent refetch={refetch} queryArgs={queryArgs} /> </React.Suspense> ); }/** * MainContent.react.js */ // Fetches and renders the query, given the fetch options function MainContent(props) { const {refetch, queryArgs} = props; const data = useLazyLoadQuery( graphql` query AppQuery($id: ID!) { user(id: $id) { name friends { count } } } `, queryArgs.variables, queryArgs.options, ); return ( <> <h1>{data.user?.name}</h1> <div>Friends count: {data.user.friends?.count}</div> <Button onClick={() => refetch()}> Fetch latest count </Button> </> ); }要点解析:
- 在事件处理器中通过更新 state 中的 query args触发重取:这会让使用
useLazyLoadQuery的MainContent以新的variables和fetchKey重新渲染,并在渲染时重新获取查询。 - 我们传入了一个每次递增的新
fetchKey。向useLazyLoadQuery传入新的fetchKey,会确保查询被完全重新求值并重新获取。 - 我们没有传入新的
fetchPolicy,因此使用默认值'store-or-network';同样可以显式指定其他策略来控制是否复用本地缓存,参见 Reusing Cached Data For Render。 - 事件处理器中的 state 更新会触发组件重新渲染,并可能导致组件挂起(机制见 Loading States with Suspense)。因此必须在包裹
MainContent的位置提供Suspense边界,以显示 fallback 加载态。
关于fetchKey与fetchPolicy的权威语义,可直接参见 packages/react-relay/relay-hooks/useLazyLoadQuery.js 中Options类型的注释:
fetchKey(第 52-54 行):传入不同的fetchKey会强制在当前组件重新渲染时重新求值查询与变量,即使变量没有变化、甚至组件没有重新挂载(类似给 React 组件传不同key会使其重新挂载)。如果fetchKey与上一次渲染不同,当前查询会针对 store 重新求值,并根据当前fetchPolicy与缓存状态决定是否重新获取。networkCacheConfig(第 56-58 行):默认{force: true},用于绕过网络层的查询响应缓存。- 从实现看,
useLazyLoadQuery内部通过useMemoOperationDescriptor构造 operation descriptor,再交由useLazyLoadQueryNode处理fetchKey、fetchPolicy与fetchObservable(第 108-125 行),从而在渲染阶段驱动数据获取。
如果希望避免 Suspense:fetchQuery先行
与useQueryLoader路径相同,useLazyLoadQuery也支持"先fetchQuery写缓存、再更新 state 读缓存"的免挂起方案:
/** * App.react.js */ const AppQuery = require('__generated__/AppQuery.graphql'); function App(props: Props) { const environment = useRelayEnvironment(); const [isRefreshing, setIsRefreshing] = useState(false) const [queryArgs, setQueryArgs] = useState({ options: {fetchKey: 0, fetchPolicy: 'store-or-network'}, variables: {id: '4'}, }); const refetch = 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. setQueryArgs(prev => ({ options: { fetchKey: (prev?.options.fetchKey ?? 0) + 1, fetchPolicy: 'store-only', }, variables: {id: 'different-id'} })); }, error: () => { setIsRefreshing(false); } }); }, [/* ... */]); return ( <React.Suspense fallback="Loading query..."> <MainContent isRefetching={isRefetching} refetch={refetch} queryArgs={queryArgs} /> </React.Suspense> ); }注:
fetchQuery(environment, AppQuery, variables)中的variables应替换为本次要获取的新变量(例如{id: 'different-id'}),与setQueryArgs中写入的变量保持一致。
要点解析:
- 重取期间我们自行维护
isRefetching状态,用它渲染忙碌 spinner 等加载 UI,而无需隐藏MainContent。 - 事件处理器中先调用
fetchQuery将数据写入本地 Relay store;待请求完成后更新 state,以新的fetchKey和fetchPolicy驱动useLazyLoadQuery重新渲染出更新后的数据。 - 此时数据已经缓存在 store 中,因此把
fetchPolicy设为'store-only'以避免挂起,只读取已缓存数据。
fetchPolicy 语义速查
无论是loadQuery、useLazyLoadQuery还是fetchQuery,fetchPolicy都决定"何时读取缓存、何时发网络请求"。四种取值及语义整理如下(依据 useLazyLoadQuery.js 中 Options 注释与 Reusing Cached Data For Render):
| fetchPolicy | 是否复用本地缓存 | 是否发网络请求 | 典型用途 |
|---|---|---|---|
store-or-network | 是 | 仅当缓存数据缺失时 | 默认策略,兼顾速度与新鲜度 |
store-and-network | 是 | 总是 | 既要缓存即时渲染,又要后台更新 |
network-only | 否 | 总是 | 强制获取最新数据(如刷新) |
store-only | 是 | 从不 | 只读缓存,避免挂起;或配合fetchQuery预取后使用 |
注意两点默认值差异:loadQuery与useLazyLoadQuery的默认策略是'store-or-network',而fetchQuery的默认策略是'network-only'(fetchQuery.js 第 138 行)。这正是"先用fetchQuery强制拉取、再用store-only读缓存"这一组合能够成立的原因。
相关阅读
- Refreshing Queries(刷新查询):用相同变量获取最新数据,与本文的"不同数据重取"互为对照
- Refetching Fragments with Different Data(用不同数据重新获取 Fragment):面向 Fragment 的对应方案
- Fetching Queries for Render(为渲染获取查询):
useQueryLoader与useLazyLoadQuery的入门讲解 - Loading States with Suspense(Suspense 加载状态):Suspense 边界与 fallback 机制
- Reusing Cached Data For Render(重用缓存数据):fetchPolicy 与缓存策略的完整讨论
- fetchQuery API 参考:
fetchQuery的参数、返回值与行为细节 - useQueryLoader 与 useLazyLoadQuery:两个 Hook 的 API 参考
【免费下载链接】relayRelay is a JavaScript framework for building>项目地址: https://gitcode.com/gh_mirrors/relay29/relay
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考