- Published on
Suspense:声明式异步加载与并发渲染
在现代 Web 应用中,异步数据请求和代码分割是构建高性能应用的基础。传统上,开发者需要在组件内部手动管理各种加载状态(isLoading, error),导致逻辑分散且容易出错。React Suspense 是 React 框架提供的一种革命性的、声明式的解决方案,它旨在从根本上改变异步操作与 UI 加载状态的管理方式。Suspense 允许开发者将组件的异步依赖(如代码或数据)与其渲染逻辑解耦,并以一种声明式的方式,在组件树中指定加载指示器,从而极大地简化了代码并优化了用户体验。
Suspense 的核心机制
组件签名与属性
Suspense 是一个由 React 提供的内置组件,它接收两个核心属性:
fallback: 一个 React 元素(通常是加载指示器,如 Spinner),在children尚未准备就绪时被渲染。children: 包含一个或多个可能“悬挂 (suspend)”的组件。
import { Suspense } from 'react';
import MyComponent from './MyComponent';
function App() {
return (
<Suspense fallback={<p>Loading component...</p>}>
<MyComponent />
</Suspense>
);
}
“悬挂 (Suspending)” 的原理
一个子组件向其上层的 <Suspense> 边界发信号,表明自己尚未准备好的方式,是在渲染期间抛出一个 Promise。
这并非一个常规的 JavaScript 错误。React 的渲染器会捕获这个被抛出的 Promise。一旦捕获到,React 会暂停该组件的渲染,并向上遍历组件树,寻找最近的 <Suspense> 祖先,然后渲染其 fallback。当这个 Promise 被 resolve 后,React 会再次尝试渲染该组件。
激活 Suspense 的数据源
并非所有异步操作都能自动激活 Suspense。只有那些与 React 的并发渲染机制集成的悬挂式数据源 (Suspense-enabled data sources) 才能触发它。
React.lazy(): 这是用于代码分割 (code splitting) 的官方 API。lazy接收一个返回动态import()的函数,并返回一个能够自动在需要时加载代码、并在加载期间“悬挂”的组件。use(Promise): 这是一个较新的 Hook,可以直接在组件中“读取”一个 Promise 的值。在 Promise 处于pending状态时,useHook 会抛出这个 Promise 来激活 Suspense。- 集成了 Suspense 的数据请求库: 现代数据请求框架(如 Relay, Next. Js App Router 的
fetch)已经原生支持 Suspense。对于 TanStack Query 等库,也提供了实验性的 Suspense 支持。
实践模式
渐进式加载 (Progressive Loading)
Suspense 组件可以任意嵌套,这使得我们可以构建出渐进式或编排式 (orchestrated) 的加载序列,从而优化用户感知性能。
机制: 通过将 UI 划分为多个嵌套的 <Suspense> 边界,可以控制不同部分的加载顺序和 fallback 的显示。外层的 Suspense 会首先展示,当其子组件加载完成后,会显示该子组件的内容,同时触发其内部嵌套的 <Suspense> 开始展示其 fallback。
避免不期望的 Fallback
在某些场景下,例如用户在搜索框中输入新内容后触发了新的数据请求,我们不希望页面立即被一个全屏的 fallback 替代,而是期望在后台加载新数据的同时,继续向用户展示旧的、已过时的数据。React 的并发特性为此提供了两个强大的 Hooks。
useTransition 与 useDeferredValueuseTransition():const [isPending, startTransition] = useTransition();此 Hook 允许你将一个状态更新标记为非紧急的“过渡 (transition)”。当你在startTransition的回调中触发一个会引起悬挂的状态更新时,React 会继续显示旧的 UI,直到新的数据加载完成,而不会立即显示fallback。isPending布尔值可以用于在旧 UI 上展示一个加载中的提示。useDeferredValue():const deferredValue = useDeferredValue(value);此 Hook 允许你“延迟”一个值的更新。它会返回该值的一个“延迟”版本,这个版本会“滞后”于最新的值。你可以用这个延迟的值来渲染旧的 UI,同时在后台用最新的值来渲染新的 UI。
useTransition 避免搜索时的 Fallbackimport React, { Suspense, useState, useTransition } from 'react';
// 这是一个假设的、会触发 Suspense 的子组件
function SearchResults({ query }) {
// 在实际应用中,这里会有一个 use(fetch(...)) 或类似的数据请求
// 为了演示,我们只返回一个简单的文本
if (query) {
return <p>Showing results for "{query}"...</p>;
}
return <p>Enter a search term</p>;
}
function SearchPage() {
const [query, setQuery] = useState('');
const [isPending, startTransition] = useTransition();
function handleChange(e) {
// 将会触发 Suspense 的状态更新,并将其标记为非紧急的“过渡”
startTransition(() => {
setQuery(e.target.value);
});
}
return (
<div>
<input
onChange={handleChange}
value={query}
placeholder="Search..."
style={{ marginBottom: '1rem' }}
/>
{/* 当 isPending 为 true (即过渡正在后台发生) 时,
这个容器会变为半透明,给用户一个视觉提示,
但其内部仍然显示着旧的 SearchResults 内容。
*/}
<div style={{ opacity: isPending ? 0.5 : 1 }}>
<Suspense fallback={<h1>🌀 Loading initial results...</h1>}>
<SearchResults query={query} />
</Suspense>
</div>
</div>
);
}
在这个例子中,当用户输入时,SearchResults 会在后台重新获取数据并渲染。在此期间,屏幕上会继续显示旧的搜索结果(并带有半透明效果),而不是 fallback 中的 <h1>🌀 Loading...</h1>。