Relay 加载状态指南:用 React Suspense 实现数据驱动的加载 UI(Loading States with Suspense)
前端开发工具【免费下载链接】relayRelay is a JavaScript framework for building>项目地址https://gitcode.com/gh_mirrors/relay29/relay点击查看免费下载Relay 的数据获取 HookusePreloadedQuery、useLazyLoadQuery在查询尚未返回时会主动挂起组件渲染本指南将围绕这一机制系统讲解如何借助 React Suspense 与 Suspense Boundary 编排加载 UI如 glimmer 占位、spinner并深入到 react-relay 的 hooks 源码 解释挂起背后的实现原理。读完本文你将掌握粗/细粒度加载边界的设计、查询/片段/分页等 API 与 Suspense 的集成方式以及用fetchQuery绕过 Suspense 的实战方案。为什么需要 Suspense数据到达前的等待渲染上一节的指南中提到usePreloadedQuery和useLazyLoadQuery会渲染从服务器获取到的查询数据但没有具体说明在数据仍在获取的过程中如何渲染加载 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 ); }这里MainContent和SecondaryContent在加载各自的异步资源时都可能挂起将它们包在同一个Suspense中可以显示单一加载状态直到它们全部就绪然后一次性渲染全部内容单次绘制。事实上MainContent和SecondaryContent挂起的原因可以各不相同不止是取数但同一个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就绪另一套直到MainContent和SecondaryContent都就绪。细粒度包裹的强大之处在于它允许其他组件在各自就绪后尽早渲染。上面的例子中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。包裹MainContent的Suspense组件检测到挂起后在MainContent就绪前也就是查询返回前渲染fallback元素此处即LoadingGlimmer。源码佐证usePreloadedQuery与useLazyLoadQuery最终都汇聚到 useLazyLoadQueryNode.js后者在渲染阶段调用QueryResource.prepareWithIdentifier(...)读取查询资源。在 QueryResource.js 中核心逻辑清晰可见如果缓存值是一个 Promise代表网络请求尚未完成就会执行throw cachedValue——这正是让 React 识别挂起并转向Suspensefallback 的标准机制如果缓存值是Error则直接抛出错误对应 Error Boundary否则返回就绪的查询结果。也就是说查询组件挂起本质上是在渲染阶段抛出一个 PromiseReact 借此暂停该子树并显示 fallback。从 useLazyLoadQuery.js 可以看到useLazyLoadQuery在渲染时通过fetchQuery(environment, query)构造fetchObservable并把fetchPolicy、fetchKey、UNSTABLE_renderPolicy一并传入useLazyLoadQueryNode而usePreloadedQueryusePreloadedQuery.js则优先复用loadQuery预取的sourceobservable仅当环境匹配时否则回退到渲染期重新执行并去重de-dupe查询。挂起与 fetchPolicy 的关系何时允许直接渲染QueryResource的_fetchAndSaveQueryQueryResource.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-networkQueryResource.js。此外Relay 使用 SuspenseResource.js 管理挂起期间的数据保留渲染阶段通过temporaryRetain临时保留数据服务端环境直接跳过并设置 5 分钟TEMPORARY_RETAIN_DURATION_MS的自动释放定时器防止组件因未提交而泄漏数据一旦组件提交commitpermanentRetain会把保留转移给组件生命周期组件卸载时再释放。片段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 labelFetching username /} UsernameComponent user{data.user} / /Suspense / ); }这一机制对嵌套片段同样成立只要渲染某片段所需的数据已在本地缓存该片段组件就能渲染无论其子/后代片段的数据是否缺失。转换Transitions刷新、重取与连接分页除了查询与片段Relay 的取数类 API同样与 Suspense 集成这些场景下 API 也会挂起刷新Refreshing与重取Refetching见数据获取介绍与刷新查询指南。例如useQueryLoader场景下事件处理器中调用loadQuery(variables, {fetchPolicy: network-only})会强制走网络、跳过本地缓存随之而来的重渲染会让usePreloadedQuery再次挂起因此必须保证MainContent外层有SuspenseBoundary 来显示 fallbackuseLazyLoadQuery场景下则通过递增fetchKey并配合network-only触发同样的挂起。连接Connections分页与连接渲染相关 APIusePaginationFragment、useRefetchableFragment等也集成 Suspense。例如 usePaginationFragment.js 内部基于useRefetchableFragmentInternal与useLoadMoreFunction实现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 fallbackLoading 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-only的loadQuery组合绕过挂起。相关延伸阅读查询与渲染、错误状态处理、部分缓存数据渲染、刷新查询。赞分享前端开发工具【免费下载链接】relayRelay is a JavaScript framework for building>项目地址https://gitcode.com/gh_mirrors/relay29/relay点击查看免费下载相关推荐Relay 加载状态指南使用 React Suspense 与 usePreloadedQuery / useLazyLoadQuery 实现数据驱动的 Loading UIRelay 加载状态指南使用 React Suspense 与 usePreloadedQuery / useLazyLoadQuery 实现数据驱动的 Lo前端开发工具Relay 加载状态指南结合 React Suspense 与 Suspense Boundary 设计数据驱动的加载 UIRelay 加载状态指南结合 React Suspense 与 Suspense Boundary 设计数据驱动的加载 UI 导读 在 Relay 应用中前端开发工具Relay 渲染指南用 Suspense 为查询渲染加载状态Loading StatesRelay 渲染指南用 Suspense 为查询渲染加载状态Loading States Relayreact relay中的 usePreloade前端开发工具创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考