讲解
Suspense 是 React 的「异步编排」机制:组件在渲染期间如果发现数据还没准备好,就「挂起」(抛出一个 Promise),React 向上找到最近的 <Suspense> 边界,显示它的 fallback;Promise 就绪后重新尝试渲染。开发者写的组件代码是「同步风格」的——const data = use(query) 直接拿到结果,加载态由边界统一声明,不再每个组件手写 isLoading 三元。
Suspense 的战略价值在「边界的摆放」。一个 <Suspense> 包住整页:任何数据没好就全页转圈(粗粒度,体验差)。按区块摆边界:头部、列表、侧边栏各自独立加载,谁先好谁先显示(细粒度,渐进式体验)。配合路由级代码分割,懒加载的组件代码下载期间也是 Suspense 接管——const Admin = lazy(() => import('./Admin')) 配 <Suspense fallback={...}> 是标准用法。
现实提醒:Suspense 的数据加载需要和「Suspense 感知」的数据源配合——直接在 useEffect 里 fetch 不会触发 Suspense。官方推荐的路径是框架集成(Next.js 的 async 组件)或支持 Suspense 的库(TanStack Query 的 suspense 模式、SWR、Relay),以及 React 19 的 use() 配合缓存的 Promise。手写一个「Promise 缓存 + throw promise」的玩具实现有助于理解,但生产代码别自己造。
示例
手写一个迷你 Suspense 数据源:throw promise 协议 + Promise 缓存(在本教程构建时被真实执行):
import assert from 'node:assert/strict';
// Suspense 感知的数据源:第一次读抛 Promise,就绪后返回数据
function createResource(fetcher) {
let status = 'pending';
let result;
const promise = fetcher().then(
(data) => {
status = 'success';
result = data;
},
(err) => {
status = 'error';
result = err;
},
);
return {
read() {
if (status === 'pending') throw promise; // 挂起协议:抛 Promise
if (status === 'error') throw result; // 错误边界接管
return result;
},
};
}
// 模拟 React 的渲染循环:挂起就等 Promise,然后重试
async function renderWithSuspense(readFn) {
for (;;) {
try {
return readFn(); // 读到了 → 渲染成功
} catch (p) {
if (p instanceof Promise) {
await p; // 挂起 → 等待 → 重试渲染
continue;
}
throw p;
}
}
}
let fetchCount = 0;
const resource = createResource(
() =>
new Promise((resolve) => {
fetchCount++;
setTimeout(() => resolve({ title: '深入 React' }), 50);
}),
);
// 第一次 read 会 throw promise
assert.throws(() => resource.read(), (e) => e instanceof Promise);
const data = await renderWithSuspense(() => resource.read());
assert.strictEqual(data.title, '深入 React');
assert.strictEqual(fetchCount, 1); // 缓存:重试渲染不会重新请求
// 之后再读同步返回
assert.strictEqual(resource.read().title, '深入 React');
console.log('首次渲染挂起,50ms 后数据就绪重试成功:', JSON.stringify(data));
console.log('Promise 缓存保证只请求一次:', fetchCount === 1);
真实代码(React 19 + use):
import { Suspense, lazy, use } from 'react';
// 路由级懒加载:下载期间 Suspense 接管
const Admin = lazy(() => import('./Admin'));
// 细粒度边界:各区块独立加载
function Page({ bookPromise }) {
return (
<>
<h1>书店</h1>
<Suspense fallback={<p>书籍加载中…</p>}>
<BookDetail promise={bookPromise} />
</Suspense>
<Suspense fallback={<p>评论加载中…</p>}>
<Comments />
</Suspense>
</>
);
}
function BookDetail({ promise }) {
const book = use(promise); // 渲染期间读 Promise,没好就挂起
return <h2>{book.title}</h2>;
}
常见坑
- 在 useEffect 里 fetch 却期待 Suspense:effect 数据流不挂起,需要 Suspense 感知的数据源(use/框架/查询库)。
- 一个边界包整页:一个慢接口拖住全页;按区块摆边界做渐进加载。
- use 的 Promise 每次渲染新建:渲染函数里
use(fetch(...))会无限请求-挂起循环;Promise 要缓存或在事件/服务端创建。 - fallback 闪烁:数据 50ms 就好时闪一下 loading 更难受,可以给 fallback 加延迟或骨架屏。
- 忘记错误边界:请求失败的 throw error 要由 ErrorBoundary 接,只摆 Suspense 不接错误会白屏。
小结
Suspense = 组件挂起 + 边界显示 fallback + 就绪后重试;粒度决定体验;数据源必须 Suspense 感知。下一章把多页面组织起来:React Router。