实战指南:useQueryLoader、useLazyLoadQuery 与 fetchQuery 三种刷新方案详解)
前端开发工具【免费下载链接】relayRelay is a JavaScript framework for building>项目地址https://gitcode.com/gh_mirrors/relay29/relay点击查看免费下载在 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 fallbackLoading 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 divFriends 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 dont 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 fallbackLoading 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.jsfetchQuery会创建 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 基本方案递增fetchKeynetwork-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 fallbackLoading 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 divFriends 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与 optionsuseLazyLoadQuery的 options 类型useLazyLoadQuery.js明确包含fetchPolicy决定是否使用本地缓存、何时发网络请求详见下节fetchKeystring | 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 dont 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 fallbackLoading 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.jsloadQuery也强制合并force: trueloadQuery.js用于绕过网络层的响应缓存。5.2fetchKey强制重新求值的查询版本号fetchKey的作用可以类比 React 组件上的key给它传一个与上次渲染不同的值即使查询与 variables 都未改变、组件也未重新挂载当前查询也会被重新求值re-evaluate并依据fetchPolicy与缓存状态决定是否重新拉取。在useQueryLoader场景下loadQuery每次调用都会自增一个内部fetchKeyloadQuery.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触发方式是否挂起关键配置再次loadQueryuseQueryLoaderusePreloadedQuery事件处理器是fetchPolicy: network-onlyfetchQueryloadQuery(store-only)同上事件处理器否先network-only写缓存再store-only读缓存递增fetchKeyuseLazyLoadQuerystate 更新是fetchKey递增 fetchPolicy: network-onlyfetchQuery state 更新store-only同上state 更新否先写缓存再更新fetchKeystore-only选择建议默认优先使用render-as-you-fetch的useQueryLoader/usePreloadedQuery组合loadQuery会在事件处理器中立即发请求性能更优且useQueryLoader会自动释放旧的queryRef避免 store 数据泄漏详见 load-query.md 与 use-preloaded-query.md如果刷新时不希望已有内容被 fallback 隐藏例如列表页下拉刷新就采用fetchQuerystore-only的两段式方案同时自行维护isRefreshing状态来显示轻量加载指示useLazyLoadQuery刷新适合简单场景但如文档与源码注释useLazyLoadQuery.js所指出的它把数据获取推迟到渲染期容易造成嵌套或瀑布式往返waterfall应谨慎使用。最后提醒所有刷新方案都要求应用根部挂载 RelayEnvironmentProvider 提供 Relay environment且loadQuery与useQueryLoader返回的loadQuery回调不能在 React 渲染阶段调用否则会抛错。按照上述方案你就能在保持 Suspense 优雅加载态与不打断已渲染内容之间做出合适的取舍。赞分享前端开发工具【免费下载链接】relayRelay 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 拉取最新数据 本篇前端开发工具上一篇Beekeeper Studio 支持的数据库全景支持矩阵、版本差异与源码级连接机制解析下一篇TDengine 零代码数据接入通过 taosExplorer 将 MySQL 数据迁移/同步至 TDengine 完整指南创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考