news 2026/9/24 1:22:35

Relay 加载状态指南:用 React Suspense 实现数据驱动的加载 UI(Loading States with Suspense)

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Relay 加载状态指南:用 React Suspense 实现数据驱动的加载 UI(Loading States with Suspense)

Relay 的数据获取 Hook(usePreloadedQueryuseLazyLoadQuery)在查询尚未返回时会主动"挂起"组件渲染,本指南将围绕这一机制,系统讲解如何借助 React Suspense 与 Suspense Boundary 编排加载 UI(如 glimmer 占位、spinner),并深入到 react-relay 的 hooks 源码 解释挂起背后的实现原理。读完本文,你将掌握粗/细粒度加载边界的设计、查询/片段/分页等 API 与 Suspense 的集成方式,以及用fetchQuery绕过 Suspense 的实战方案。

为什么需要 Suspense:数据到达前的"等待渲染"

上一节的指南中提到,usePreloadedQueryuseLazyLoadQuery会渲染从服务器获取到的查询数据,但没有具体说明:在数据仍在获取的过程中,如何渲染加载 UI(例如一个 glimmer)。这正是本节要解决的问题。

Relay 渲染加载状态依赖React Suspense。Suspense 是 React 的一项能力:它允许组件中断(或者说"挂起")渲染,以便等待某个异步资源(代码、图片或数据)加载完成。当组件"挂起"时,它向 React 表明该组件"尚未准备好"被渲染,直到它等待的异步资源加载完毕;资源加载完成后,React 会再次尝试渲染该组件。

这项能力对组件表达"渲染所需"的异步依赖(数据、代码、图片)非常有用,它让 React 能够跨组件树协调加载状态的渲染,直到这些异步资源就绪。更一般地,使用 Suspense 让我们在应用首次加载、或在不同状态之间过渡时,能更精确地设计加载状态,并避免加载序列未经设计时常见的加载元素(如 spinner)意外闪烁问题。

用 Suspense Boundary 渲染加载回退(fallback)

当一个组件挂起时,我们需要在等待其"就绪"期间,用fallback占位内容替换它渲染。为此我们使用 React 提供的Suspense组件:

const React = require('React'); const {Suspense} = require('React'); function App() { return ( // 使用 Suspense 作为包裹层渲染 fallback <Suspense fallback={<LoadingGlimmer />}> <CanSuspend /> </Suspense> ); }

Suspense组件可以包裹任意组件;如果目标组件挂起,Suspense会渲染提供的fallback,直到其所有后代都"就绪"(即子树中被挂起的所有组件全部解析)。通常,fallback用于渲染 glimmer、占位符之类的加载状态。

应用中往往有多个不同的内容区块可能挂起,我们可以分别为它们用Suspense提供加载状态:

/** * App.react.js */ const React = require('React'); const {Suspense} = require('React'); function App() { return ( // LoadingGlimmer 通过 Suspense 的 fallback 渲染 <Suspense fallback={<LoadingGlimmer />}> <MainContent /> {/* MainContent 可能会挂起 */} </Suspense> ); }

这里的运行机制可以这样拆解:

  • 如果MainContent因为等待某个异步资源(如数据)而挂起,包裹它的Suspense组件会检测到挂起,并在MainContent就绪前渲染fallback元素(此处即LoadingGlimmer)。注意这也会传递性地包含MainContent的后代——它们也可能挂起。

粗粒度加载:合并多个子树的加载状态

Suspense 的一个优点在于,你可以对组件树的不同部分分别累积加载状态,进行粒度控制:

/** * App.react.js */ const React = require('React'); const {Suspense} = require('React'); function App() { return ( // 所有内容共用一个 LoadingGlimmer,通过 Suspense 的 fallback 渲染 <Suspense fallback={<LoadingGlimmer />}> <MainContent /> <SecondaryContent /> {/* SecondaryContent 也可能挂起 */} </Suspense> ); }
  • 这里MainContentSecondaryContent在加载各自的异步资源时都可能挂起;将它们包在同一个Suspense中,可以显示单一加载状态,直到它们全部就绪,然后一次性渲染全部内容(单次绘制)。
  • 事实上,MainContentSecondaryContent挂起的原因可以各不相同(不止是取数),但同一个Suspense都能在子树内所有组件就绪前渲染 fallback。同样,这也传递性地涵盖二者的后代。

细粒度加载:让先就绪的内容先渲染

反过来,你也可以决定把加载 UI 做得更细,用Suspense包裹组件树中更小或独立的部分:

/** * App.react.js */ const React = require('React'); const {Suspense} = require('React'); function App() { return ( <> {/* 为 LeftHandColumn 显示独立的加载 UI */} <Suspense fallback={<LeftColumnPlaceholder />}> <LeftColumn /> </Suspense> {/* 为 Main 和 Secondary 内容显示另一个独立的加载 UI */} <Suspense fallback={<LoadingGlimmer />}> <MainContent /> <SecondaryContent /> </Suspense> </> ); }
  • 此时我们看到两套独立的加载 UI:一套直到LeftColumn就绪,另一套直到MainContentSecondaryContent都就绪。
  • 细粒度包裹的强大之处在于:它允许其他组件在各自就绪后尽早渲染。上面的例子中,LeftColumn一旦就绪就能立即渲染,而不必等待内容区域,这可能比内容区就绪的时间更早。

过渡(Transitions)与更新的挂起

SuspenseBoundary 的 fallback 让我们能描述初次渲染某个内容时的加载占位;但应用还会有不同内容之间的过渡。具体而言,当你在一个已经挂载的 Boundary 内切换两个组件时,新切换到的组件可能还没加载完它的所有异步依赖,因此它也可能挂起。

在这些场景下,我们仍然会显示SuspenseBoundary 的 fallback。但这也意味着:为了显示 fallback,我们会隐藏已渲染的现有内容。在后续支持并发渲染的 React 版本中,React 将提供选项来避免这种情况——挂起时不会用 Suspense fallback 隐藏已经渲染好的内容。

Relay 如何在内部使用 Suspense

查询(Queries):查询组件是可挂起组件

在 Relay 中,查询组件就是会挂起的组件,因此我们用 Suspense 在查询获取期间渲染加载状态。假设我们有下面这样的查询渲染组件:

/** * MainContent.react.js * * Query Component(查询组件) */ const React = require('React'); const {graphql, usePreloadedQuery} = require('react-relay'); function MainContent(props) { // 获取并渲染一条查询 const data = usePreloadedQuery( graphql`...`, props.queryRef, ); return (...); }
/** * App.react.js */ const React = require('React'); const {Suspense} = require('React'); function App() { return ( // LoadingGlimmer 通过 Suspense 的 fallback 渲染 <Suspense fallback={<LoadingGlimmer />}> <MainContent /> {/* MainContent 可能会挂起 */} </Suspense> ); }

这里的运行机制是:

  • MainContent是查询渲染器,负责获取并渲染查询。当它尝试获取查询时,会挂起渲染,表明自己尚未准备好;查询获取完成后,它才会解析(resolve)。
  • 包裹MainContentSuspense组件检测到挂起后,在MainContent就绪前(也就是查询返回前)渲染fallback元素(此处即LoadingGlimmer)。

源码佐证usePreloadedQueryuseLazyLoadQuery最终都汇聚到 useLazyLoadQueryNode.js,后者在渲染阶段调用QueryResource.prepareWithIdentifier(...)读取查询资源。在 QueryResource.js 中,核心逻辑清晰可见:如果缓存值是一个 Promise(代表网络请求尚未完成),就会执行throw cachedValue——这正是让 React 识别"挂起"并转向Suspensefallback 的标准机制;如果缓存值是Error则直接抛出错误(对应 Error Boundary);否则返回就绪的查询结果。也就是说,"查询组件挂起"本质上是在渲染阶段抛出一个 Promise,React 借此暂停该子树并显示 fallback。

从 useLazyLoadQuery.js 可以看到,useLazyLoadQuery在渲染时通过fetchQuery(environment, query)构造fetchObservable,并把fetchPolicyfetchKeyUNSTABLE_renderPolicy一并传入useLazyLoadQueryNode;而usePreloadedQuery(usePreloadedQuery.js)则优先复用loadQuery预取的sourceobservable(仅当环境匹配时),否则回退到渲染期重新执行并去重(de-dupe)查询。

挂起与 fetchPolicy 的关系:何时允许直接渲染

QueryResource_fetchAndSaveQuery(QueryResource.js)根据fetchPolicy决定"是否需要发起网络请求"与"是否允许立即渲染":

fetchPolicyshouldFetch(是否请求网络)shouldAllowRender(是否允许立即渲染)
store-only
store-or-network数据不完整时才请求数据完整或启用 partial render 时允许
store-and-network总是请求数据完整或启用 partial render 时允许
network-only(默认分支)总是请求否(必须等网络返回)

shouldAllowRender为 false 时,Relay 会把该查询的 Promise 缓存起来(QueryResource.js),并给这个 Promise 打上displayName = 'Relay(' + operation.fragment.node.name + ')'便于调试——这正对应"在此查询根节点挂起"。DEFAULT_FETCH_POLICY = 'store-or-network'是查询的默认策略,而 live query(含执行期 resolver)默认使用store-and-network(QueryResource.js)。

此外,Relay 使用 SuspenseResource.js 管理挂起期间的数据保留:渲染阶段通过temporaryRetain临时保留数据(服务端环境直接跳过),并设置 5 分钟(TEMPORARY_RETAIN_DURATION_MS)的自动释放定时器,防止组件因未提交而泄漏数据;一旦组件提交(commit),permanentRetain会把保留转移给组件生命周期,组件卸载时再释放。

片段(Fragments):部分渲染的边界

片段同样与 Suspense 集成,以支持渲染@defer延迟的数据,或渲染 Relay Store 中部分可用的数据(即部分渲染(partial rendering))。

关键规则(详见部分渲染指南):

  • 片段组件在渲染时,如果它本地声明的数据缺失且正在获取,就会挂起,直到其所属查询(父查询)获取完成;
  • 数据缺失只按"本地声明的字段"判定——片段通过片段展开(fragment spread)引入的子片段数据缺失,不会影响外层片段/查询的缺失判定;
  • 因此,如果查询本身的数据已缓存,可以立即渲染外层内容,而把缺失数据的子片段单独用Suspense包裹,实现"先渲染已缓存部分,再等缺失部分",从而跳过加载状态:
/** * HomeTab.react.js * * Query Component */ const React = require('React'); const {Suspense} = require('React'); const {graphql, usePreloadedQuery} = require('react-relay'); const UsernameComponent = require('./UsernameComponent.react'); function HomeTab() { const data = usePreloadedQuery( graphql` query AppQuery($id: ID!) { user(id: $id) { name ...UsernameComponent_user } } `, props.queryRef, ); return ( <> <h1>{data.user?.name}</h1> {/* 用 Suspense 包裹 UserComponent,即使 username 缺失, 也允许 App 的其他部分继续渲染。 */} <Suspense fallback={<LoadingSpinner label="Fetching username" />}> <UsernameComponent user={data.user} /> </Suspense> </> ); }

这一机制对嵌套片段同样成立:只要渲染某片段所需的数据已在本地缓存,该片段组件就能渲染,无论其子/后代片段的数据是否缺失。

转换(Transitions):刷新、重取与连接分页

除了查询与片段,Relay 的取数类 API同样与 Suspense 集成,这些场景下 API 也会挂起:

  • 刷新(Refreshing)与重取(Refetching):见数据获取介绍与刷新查询指南。例如useQueryLoader场景下,事件处理器中调用loadQuery(variables, {fetchPolicy: 'network-only'})会强制走网络、跳过本地缓存,随之而来的重渲染会让usePreloadedQuery再次挂起,因此必须保证MainContent外层有SuspenseBoundary 来显示 fallback;useLazyLoadQuery场景下,则通过递增fetchKey并配合network-only触发同样的挂起。
  • 连接(Connections):分页与连接渲染相关 API(usePaginationFragmentuseRefetchableFragment等)也集成 Suspense。例如 usePaginationFragment.js 内部基于useRefetchableFragmentInternaluseLoadMoreFunction实现loadNext/loadPrevious/refetch,其重取路径同样会在新数据未就绪时挂起(useRefetchableFragmentInternal.js 通过useQueryLoader/fetchQuery发起新请求)。

进阶实战:需要避免挂起时的替代方案

Suspense 的挂起会隐藏已渲染内容来显示 fallback。在某些场景(例如刷新数据时不希望整块内容闪成 spinner),需要避开挂起。官方推荐的做法是改用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 会获取查询并把数据写入 Relay Store, // 这样重渲染时数据已在本地缓存,从而不会挂起 fetchQuery(environment, AppQuery, variables) .subscribe({ complete: () => { setIsRefreshing(false); // *在* 查询获取完成之后,再调用 loadQuery 生成新的 queryRef。 // 此时查询数据已在缓存中,使用 'store-only' fetchPolicy 避免挂起。 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,而不会隐藏MainContent
  • 事件处理器先调用fetchQuery把数据写入本地 Relay Store,请求完成后调用loadQuery拿到新的queryRef再传给usePreloadedQuery渲染新数据;
  • 由于此时数据已在缓存中,loadQuery使用'store-only'策略只读缓存,避免挂起。

小结:Suspense 加载状态的正确姿势

  • <Suspense fallback={...}>包裹可能挂起的查询/片段组件,fallback 渲染 glimmer 或占位符;
  • 一个Suspense合并多个区块的加载状态(粗粒度),或用多个Suspense让先就绪的内容先渲染(细粒度),避免闪烁;
  • 记住 Relay 的挂起机制(QueryResource.js 中"缓存 Promise 并 throw"),理解fetchPolicy如何影响是否立即渲染;
  • 刷新、重取、分页等取数 API 同样会挂起,记得在对应组件外层保留SuspenseBoundary;
  • 需要保留已渲染内容时,用fetchQuery+ 手动 loading 状态 +store-onlyloadQuery组合绕过挂起。

相关延伸阅读:查询与渲染、错误状态处理、部分缓存数据渲染、刷新查询。

  • 前端
  • 开发工具

【免费下载链接】relay

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

点击查看免费下载

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

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

户外监控摄像头起雾结露怎么办?电解除湿膜全天候控湿方案解析

一、户外监控的隐形敌人&#xff1a;内部起雾与凝露智慧城市建设推进至今&#xff0c;户外监控探头已经遍布道路、园区、山区、海岸。但一线运维人员都清楚一个顽疾 ——摄像头内部起雾。昼夜温差大、雨雾频繁的环境下&#xff0c;密闭的摄像头防护罩内极易产生结露。水汽附着在…

作者头像 李华
网站建设 2026/9/24 1:09:09

腾讯云轻量服务器免费升配技术解析与实操指南

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

作者头像 李华
网站建设 2026/9/24 1:08:38

Python机器学习文本分类器实战:从数据清洗到模型部署全流程

简介&#xff1a;这份资源是面向NLP入门者与机器学习实践者的Python文本分类项目包&#xff0c;围绕文本自动归类这一核心任务&#xff0c;覆盖从数据预处理、特征工程到模型训练与评估的完整链路。包内共30个文件&#xff0c;以27个py脚本为主体&#xff0c;辅以1个md说明、1个…

作者头像 李华
网站建设 2026/9/24 1:07:27

Python股票K线数据校验与清洗:量化回测避坑指南

说实话&#xff0c;我用 Python 折腾股票历史 K 线的这几年&#xff0c;最深的感触是&#xff1a;大多数量化策略跑出来的结果很漂亮&#xff0c;但一模拟盘就露馅&#xff0c;问题往往不在策略逻辑&#xff0c;而在最底层的 K 线数据压根没洗干净。很多人以为从 AkShare、Tush…

作者头像 李华