Suspense 与加载
本教程共 50 篇 · 第 37 篇 · 更新于 2026-07-29 · 约 8 分钟阅读
37. Suspense 与加载
本节目标:理解 Suspense 怎么工作,学会用它优雅地处理加载态,不再到处写
if (isLoading)。
Suspense 是什么
以前处理加载态,你到处写这种代码:
function Page() {
const [data, setData] = useState(null);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => { /* 请求 */ }, []);
if (isLoading) return <Spinner />;
return <Content data={data} />;
}
每个组件都来一遍,又丑又容易出错。Suspense 的思路是:声明式地告诉 React”这里有异步操作,没完成时显示这个 fallback”。
<Suspense fallback={<Spinner />}>
<Page />
</Suspense>
子组件在”等待”什么的时候,React 会自动显示 fallback。等子组件准备好了,再替换成实际内容。
什么能触发 Suspense
Suspense 不是万能的,它只对特定的”等待”有反应:
React.lazy懒加载的组件 —— 代码还没下载完use(Promise)—— React 19 的 Hook,读取 Promise 的值- 支持 Suspense 的数据源 —— 比如框架内置的 fetch、TanStack Query 的 suspense 模式
普通的 useEffect + fetch 不会触发 Suspense。这是新手最容易搞混的地方。
NoteSuspense 不是”自动检测所有异步操作”。它需要组件主动”抛出”Promise,React 才能捕获到。
React.lazy 与代码分割
React.lazy 让你按需加载组件,配合 Suspense 显示加载态:
import { lazy, Suspense } from 'react';
const HeavyChart = lazy(() => import('./HeavyChart.js'));
function Dashboard() {
return (
<div>
<h1>仪表盘</h1>
<Suspense fallback={<p>图表加载中……</p>}>
<HeavyChart />
</Suspense>
</div>
);
}
import('./HeavyChart.js') 是动态导入,Vite 会把 HeavyChart 拆成单独的 JS 文件。只有用户访问到这个页面时才会下载。
Warning
lazy必须在组件外部声明,不能在组件函数内部。否则每次渲染都会创建新的组件类型,导致状态丢失。
嵌套 Suspense
Suspense 可以嵌套,每个边界独立管理自己的 fallback:
<Suspense fallback={<p>整个页面加载中……</p>}>
<Header />
<main>
<Suspense fallback={<p>图表加载中……</p>}>
<Chart />
</Suspense>
<Suspense fallback={<p>列表加载中……</p>}>
<DataList />
</Suspense>
</main>
</Suspense>
好处是各模块独立加载,不用等全部数据到齐才显示页面。用户先看到 Header,然后图表和列表陆续出现。
Suspense 与 use Hook 配合
React 19 的 use Hook 是 Suspense 的最佳搭档:
import { use, Suspense } from 'react';
function Message({ messagePromise }) {
const content = use(messagePromise);
return <p>{content}</p>;
}
function App() {
return (
<Suspense fallback={<p>加载消息中……</p>}>
<Message messagePromise={fetchMessage()} />
</Suspense>
);
}
use(messagePromise) 会暂停组件的渲染,等 Promise 完成后再继续。最近的 Suspense 边界显示 fallback。
Tip传给
use的 Promise 必须是缓存过的。每次渲染创建新 Promise 会导致无限循环——组件挂起、重渲染、又创建新 Promise、又挂起。
避免”瀑布”问题
Suspense 虽然好看,但用不好容易产生瀑布——串行加载:
// ❌ 瀑布:等 User 加载完才开始加载 Posts
function Page() {
return (
<Suspense fallback={<Spinner />}>
<User />
<Posts />
</Suspense>
);
}
如果 User 和 Posts 各自有异步依赖,它们会串行执行。解决办法是并行触发:
// ✅ 并行:两个 Suspense 独立加载
function Page() {
return (
<div>
<Suspense fallback={<UserSkeleton />}>
<User />
</Suspense>
<Suspense fallback={<PostsSkeleton />}>
<Posts />
</Suspense>
</div>
);
}
或者把 Promise 提前创建,在组件外部就开始请求:
// ✅ 请求在渲染前就开始了
const userPromise = fetchUser();
const postsPromise = fetchPosts();
function Page() {
return (
<div>
<Suspense fallback={<UserSkeleton />}>
<User promise={userPromise} />
</Suspense>
<Suspense fallback={<PostsSkeleton />}>
<Posts promise={postsPromise} />
</Suspense>
</div>
);
}
Suspense 的 fallback 细节
fallback 可以是任何 JSX:
<Suspense fallback={
<div className="skeleton">
<div className="skeleton-line" />
<div className="skeleton-line" />
<div className="skeleton-line" />
</div>
}>
<HeavyComponent />
</Suspense>
但注意:fallback 本身不能包含 Suspense 内容。如果 fallback 也需要异步加载,React 会报错。
什么时候不该用 Suspense
Suspense 不是银弹。以下场景用传统 loading state 更合适:
- 表单提交:用户点提交按钮后等待响应,用
isPending或useFormStatus更合适 - 搜索输入:用户每敲一个键就发请求,用
useDeferredValue防抖更合适 - 局部更新:页面大部分已显示,只更新一小块数据,用局部 loading
Suspense 最适合的场景是路由切换和首屏加载——整块内容需要等数据到了才能渲染。
TipReact 19 的
useTransition配合 Suspense 可以做到”不显示 fallback,保持旧内容直到新内容就绪”。详见 §39。