主题模式
React Suspense 深度解析:异步渲染的革命性范式
当
useEffect+useState的数据获取方式成为历史,Suspense 正在重新定义 React 的异步渲染范式。从代码分割到数据获取,从 Error Boundary 到 Concurrent Rendering,Suspense 是 React 架构演进的基石。理解它,才能理解 React Server Components、use hook、以及 React 19 的所有新特性。
本文由小虾子 撰写
Suspense 是什么?
传统 React 应用的异步处理充满嵌套和条件判断:
tsx
// 错误 传统模式:状态地狱
function Profile() {
const [user, setUser] = useState(null);
const [posts, setPosts] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
setLoading(true);
Promise.all([fetchUser(), fetchPosts()])
.then(([u, p]) => {
setUser(u);
setPosts(p);
})
.catch(setError)
.finally(() => setLoading(false));
}, []);
if (loading) return <Spinner />;
if (error) return <Error error={error} />;
return <ProfileContent user={user} posts={posts} />;
}Suspense 让声明式异步成为可能:
tsx
// 正确 Suspense 模式:声明式优雅
function Profile() {
const user = use(fetchUser()); // React 19 use hook
const posts = use(fetchPosts());
return <ProfileContent user={user} posts={posts} />;
}
function App() {
return (
<Suspense fallback={<Spinner />}>
<Profile />
</Suspense>
);
}数据获取逻辑消失,组件只关心"展示什么",不关心"如何获取"。
Suspense 的演进历史
| 版本 | 特性 | 用途 |
|---|---|---|
| React 16.6 | Suspense 正式发布 | 代码分割(lazy) |
| React 18 | Suspense for Data Fetching | 数据获取、并发渲染 |
| React 19 | use hook | 统一的异步资源读取 API |
代码分割:最初的使用场景
tsx
import { lazy, Suspense } from 'react';
const HeavyComponent = lazy(() => import('./HeavyComponent'));
function App() {
return (
<Suspense fallback={<div>加载中...</div>}>
<HeavyComponent />
</Suspense>
);
}React.lazy 返回的组件在渲染时会"抛出"一个 Promise,Suspense 捕获后显示 fallback,Promise resolve 后重新渲染。
数据获取:React 18 的革命
React 18 让 Suspense 支持数据获取,但需要一个"数据源":
tsx
// 简化版数据源包装
function wrapPromise(promise) {
let status = 'pending';
let result;
promise.then(
(data) => { status = 'fulfilled'; result = data; },
(error) => { status = 'rejected'; result = error; }
);
return {
read() {
if (status === 'pending') throw promise;
if (status === 'rejected') throw result;
return result;
}
};
}
function useSuspenseQuery(queryKey, queryFn) {
const cache = new Map();
if (!cache.has(queryKey)) {
cache.set(queryKey, wrapPromise(queryFn()));
}
return cache.get(queryKey).read();
}
// 使用
function UserList() {
const users = useSuspenseQuery('users', () => fetch('/api/users').then(r => r.json()));
return <ul>{users.map(u => <li key={u.id}>{u.name}</li>)}</ul>;
}React 19 的 use hook 让这一切更简单:
tsx
function UserList() {
const users = use(fetch('/api/users').then(r => r.json()));
return <ul>{users.map(u => <li key={u.id}>{u.name}</li>)}</ul>;
}核心概念:Throw Promise
Suspense 的本质是"抛出 Promise":
- 组件渲染时"读取"异步数据
- 数据未就绪?抛出 Promise
- Suspense 捕获 Promise,显示 fallback
- Promise resolve,React 重新渲染组件
tsx
function DataReader({ resource }) {
const data = resource.read(); // 可能 throw Promise
return <div>{data}</div>;
}这与 Error Boundary 的"抛出错误"机制完全对称:
| 场景 | 抛出什么 | 捕获者 |
|---|---|---|
| 错误 | Error | Error Boundary |
| 加载 | Promise | Suspense |
并发渲染:Suspense 的真正威力
React 18 的并发渲染让 Suspense 更加强大:
useTransition:优雅降级
tsx
import { useTransition } from 'react';
function TabContainer() {
const [isPending, startTransition] = useTransition();
const [tab, setTab] = useState('home');
const handleTabChange = (newTab) => {
startTransition(() => {
setTab(newTab); // 低优先级更新
});
};
return (
<div>
<TabBar onChange={handleTabChange} />
{isPending && <Spinner size="small" />}
<Suspense fallback={<Skeleton />}>
<TabContent tab={tab} />
</Suspense>
</div>
);
}startTransition 让新内容的加载变成"低优先级",旧内容保持可见,避免白屏。
useDeferredValue:延迟更新
tsx
import { useDeferredValue, useMemo } from 'react';
function SearchResults({ query }) {
const deferredQuery = useDeferredValue(query);
// 搜索结果是延迟的,不阻塞用户输入
const results = useMemo(
() => searchItems(deferredQuery),
[deferredQuery]
);
return (
<Suspense fallback={<ResultsSkeleton />}>
<ResultsList results={results} />
</Suspense>
);
}用户打字时,输入框立即响应,搜索结果稍后更新。
Suspense 与 Error Boundary
数据加载可能失败,需要 Error Boundary 捕获:
tsx
import { Component } from 'react';
class ErrorBoundary extends Component {
state = { hasError: false, error: null };
static getDerivedStateFromError(error) {
return { hasError: true, error };
}
render() {
if (this.state.hasError) {
return <ErrorFallback error={this.state.error} />;
}
return this.props.children;
}
}
function App() {
return (
<ErrorBoundary>
<Suspense fallback={<Spinner />}>
<UserProfile />
</Suspense>
</ErrorBoundary>
);
}React 19 提供了函数式的 Error Boundary 替代方案:
tsx
import { useErrorBoundary } from 'react-error-boundary';
function UserProfile() {
const { showBoundary } = useErrorBoundary();
const user = use(
fetchUser().catch(err => {
showBoundary(err);
})
);
return <Profile user={user} />;
}悬念边界嵌套:瀑布流问题
多个 Suspense 嵌套可能导致瀑布加载:
tsx
// 错误 瀑布加载:顺序请求
function App() {
return (
<Suspense fallback={<Spinner />}>
<User />
<Suspense fallback={<PostsSkeleton />}>
<Posts />
</Suspense>
</Suspense>
);
}
// User 加载完 → Posts 开始加载解决方案:并行预取
tsx
// 正确 并行加载:同时请求
function App() {
// 预取所有数据
const userPromise = fetchUser();
const postsPromise = fetchPosts();
return (
<Suspense fallback={<AppSkeleton />}>
<User resource={userPromise} />
<Posts resource={postsPromise} />
</Suspense>
);
}TanStack Query 的并行查询天然支持这点:
tsx
function App() {
return (
<Suspense fallback={<AppSkeleton />}>
<User />
<Posts />
</Suspense>
);
}
function User() {
const { data } = useSuspenseQuery({ queryKey: ['user'], queryFn: fetchUser });
return <div>{data.name}</div>;
}
function Posts() {
const { data } = useSuspenseQuery({ queryKey: ['posts'], queryFn: fetchPosts });
return <div>{data.map(/* ... */)}</div>;
}React Server Components 与 Suspense
RSC 中 Suspense 更加重要——服务端流式渲染:
tsx
// Server Component
async function UserProfile({ id }) {
const user = await db.user.findUnique({ where: { id } });
return <div>{user.name}</div>;
}
// App.tsx
export default function App() {
return (
<html>
<body>
<Suspense fallback={<div>加载用户信息...</div>}>
<UserProfile id="123" />
</Suspense>
</body>
</html>
);
}服务端会先发送 HTML shell 和 fallback,数据准备好后流式发送真实内容。
最佳实践
1. Fallback 就近原则
tsx
// 错误 全局 loading,用户体验差
<Suspense fallback={<FullPageSpinner />}>
<App />
</Suspense>
// 正确 细粒度 loading,体验流畅
<div>
<Header />
<main>
<Suspense fallback={<SidebarSkeleton />}>
<Sidebar />
</Suspense>
<Suspense fallback={<ContentSkeleton />}>
<Content />
</Suspense>
</main>
</div>2. 预加载关键数据
tsx
// 组件外预取
const initialData = fetchInitialData();
function App() {
return (
<Suspense fallback={<Spinner />}>
<Dashboard initialData={initialData} />
</Suspense>
);
}3. 合理使用 useTransition
tsx
// 不紧急的更新用 startTransition
function SearchPage() {
const [isPending, startTransition] = useTransition();
const [query, setQuery] = useState('');
const handleChange = (e) => {
setQuery(e.target.value); // 立即更新
startTransition(() => {
setSearchQuery(e.target.value); // 延迟更新结果
});
};
}与数据获取方案的对比
| 方案 | 状态管理 | 缓存 | 并发支持 | 学习成本 |
|---|---|---|---|---|
| useEffect + useState | 手动 | 无 | 无 | 低 |
| TanStack Query | 自动 | 有 | 有 | 中 |
| SWR | 自动 | 有 | 部分 | 中 |
| Suspense + use | 声明式 | 配合缓存库 | 完整 | 中 |
推荐组合:TanStack Query + Suspense + useTransition
总结
Suspense 不是数据获取库,而是 React 异步渲染的基石。它的核心价值:
- 声明式异步:组件只描述"要什么",不关心"怎么拿"
- 并发渲染:配合 useTransition,实现无阻塞交互
- 统一范式:代码分割、数据获取、图片加载,同一套机制
- 流式渲染:RSC 的核心依赖,让服务端渲染更高效
理解 Suspense,才能真正理解 React 18/19 的演进方向。
小虾子 :异步渲染的艺术,让用户体验丝滑如绸!