学习路径
高级
Suspense 深入
学习如何使用 Suspense 处理异步操作,优化加载体验
Suspense 概述
Suspense 让组件"等待"某些操作完成,期间显示加载状态。它是 React 并发特性的核心,用于处理数据获取、代码分割等异步操作。
核心概念
- 声明式加载状态:无需手动管理 isLoading 状态
- 自动处理:React 自动处理数据获取和渲染
- 并发渲染:利用 React 18 的并发特性
- 可组合:可以嵌套多个 Suspense
TypeScript
import { Suspense } from 'react';
function DataComponent() {
const data = fetchData(); // 抛出 Promise 时 Suspense 会捕获
return <div>{data.name}</div>;
}
function App() {
return (
<Suspense fallback={<div>加载中...</div>}>
<DataComponent />
</Suspense>
);
}
Suspense 基础用法
包裹异步组件
TypeScript
import { Suspense } from 'react';
// 异步组件
function UserProfile({ userId }: { userId: number }) {
const user = fetchUser(userId); // 假设这个函数返回数据或抛出 Promise
return (
<div>
<h1>{user.name}</h1>
<p>{user.email}</p>
</div>
);
}
// 使用 Suspense
function App() {
return (
<Suspense fallback={<Spinner />}>
<UserProfile userId={1} />
</Suspense>
);
}
嵌套 Suspense
TypeScript
function App() {
return (
<div className="layout">
{/* 外层 Suspense */}
<Suspense fallback={<PageSkeleton />}>
<Header />
<div className="content">
{/* 内层 Suspense */}
<Suspense fallback={<SidebarSkeleton />}>
<Sidebar />
</Suspense>
<main>
<Suspense fallback={<ContentSkeleton />}>
<Content />
</Suspense>
</main>
</div>
<Suspense fallback={<FooterSkeleton />}>
<Footer />
</Suspense>
</Suspense>
</div>
);
}
嵌套 Suspense 的优势
- 独立加载:每个部分独立显示加载状态
- 渐进式渲染:内容准备好就立即显示
- 更好的用户体验:不会让整个页面等待
Suspense 与数据获取
Suspense 需要与支持 Promise 的数据获取库配合使用。
创建数据获取工具
TypeScript
// 创建资源函数
function createResource<T>(fetcher: () => Promise<T>) {
let status: 'pending' | 'success' | 'error' = 'pending';
let result: T;
let error: Error;
const promise = fetcher()
.then(data => {
status = 'success';
result = data;
})
.catch(err => {
status = 'error';
error = err;
});
return {
read(): T {
if (status === 'pending') {
throw promise; // 抛出 Promise 让 Suspense 捕获
}
if (status === 'error') {
throw error;
}
return result;
}
};
}
// 使用
const userResource = createResource(() =>
fetch('/api/user/1').then(res => res.json())
);
function UserProfile() {
const user = userResource.read();
return <div>{user.name}</div>;
}
使用 Relay/SWR
TypeScript
// 使用 Relay (内置 Suspense 支持)
import { graphql, usePreloadedQuery } from 'react-relay';
function UserProfile({ queryRef }) {
const data = usePreloadedQuery(
graphql`
query UserProfileQuery {
user(id: "1") {
name
email
}
}
`,
queryRef
);
return <div>{data.user.name}</div>;
}
// 使用 SWR 2.0+ (支持 Suspense)
import useSWR from 'swr';
function UserProfile() {
const { data } = useSWR('/api/user/1', fetcher, {
suspense: true // 启用 Suspense 模式
});
return <div>{data.name}</div>;
}
实际示例:用户列表
TypeScript
// 资源缓存
const resourceCache = new Map<string, any>();
function fetchUsers() {
const cacheKey = 'users';
if (resourceCache.has(cacheKey)) {
return resourceCache.get(cacheKey);
}
const resource = createResource(() =>
fetch('/api/users').then(res => res.json())
);
resourceCache.set(cacheKey, resource);
return resource;
}
function UserList() {
const users = fetchUsers().read();
return (
<ul>
{users.map((user: User) => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}
function App() {
return (
<Suspense fallback={<div>加载用户列表...</div>}>
<UserList />
</Suspense>
);
}
Suspense 与代码分割
React.lazy 和 Suspense 配合使用,可以实现组件级别的代码分割。
基础代码分割
TypeScript
import { lazy, Suspense } from 'react';
// 懒加载组件
const HeavyComponent = lazy(() => import('./HeavyComponent'));
const Chart = lazy(() => import('./Chart'));
const Editor = lazy(() => import('./Editor'));
function Dashboard() {
const [activeTab, setActiveTab] = useState('overview');
return (
<div>
<nav>
<button onClick={() => setActiveTab('overview')}>概览</button>
<button onClick={() => setActiveTab('chart')}>图表</button>
<button onClick={() => setActiveTab('editor')}>编辑器</button>
</nav>
<Suspense fallback={<div>加载中...</div>}>
{activeTab === 'overview' && <HeavyComponent />}
{activeTab === 'chart' && <Chart />}
{activeTab === 'editor' && <Editor />}
</Suspense>
</div>
);
}
错误边界集成
TypeScript
import { lazy, Suspense } from 'react';
import { ErrorBoundary } from 'react-error-boundary';
const LazyComponent = lazy(() => import('./LazyComponent'));
function App() {
return (
<ErrorBoundary
fallback={
<div>
加载失败,请刷新页面重试
<button onClick={() => window.location.reload()}>刷新</button>
</div>
}
>
<Suspense fallback={<Spinner />}>
<LazyComponent />
</Suspense>
</ErrorBoundary>
);
}
代码分割最佳实践
- 按路由分割:不同页面的组件分开加载
- 按功能分割:大型组件按功能模块分割
- 按条件分割:不常用的功能延迟加载
- 预加载:使用 webpackPrefetch 预加载
Suspense 与错误边界
Suspense 只处理加载状态,错误需要通过 Error Boundary 处理。
创建错误边界
TypeScript
interface ErrorBoundaryState {
hasError: boolean;
error?: Error;
}
class ErrorBoundary extends React.Component<
{ children: React.ReactNode },
ErrorBoundaryState
> {
constructor(props: any) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError(error: Error): ErrorBoundaryState {
return { hasError: true, error };
}
render() {
if (this.state.hasError) {
return (
<div>
<h2>出错了!</h2>
<p>{this.state.error?.message}</p>
<button onClick={() => window.location.reload()}>
刷新页面
</button>
</div>
);
}
return this.props.children;
}
}
// 使用
function App() {
return (
<ErrorBoundary>
<Suspense fallback={<Spinner />}>
<AsyncComponent />
</Suspense>
</ErrorBoundary>
);
}
使用 react-error-boundary
TypeScript
import { ErrorBoundary } from 'react-error-boundary';
function ErrorFallback({ error, resetErrorBoundary }: {
error: Error;
resetErrorBoundary: () => void;
}) {
return (
<div role="alert">
<h2>出错了!</h2>
<pre>{error.message}</pre>
<button onClick={resetErrorBoundary}>重试</button>
</div>
);
}
function App() {
return (
<ErrorBoundary FallbackComponent={ErrorFallback}>
<Suspense fallback={<Spinner />}>
<AsyncComponent />
</Suspense>
</ErrorBoundary>
);
}
Suspense 与并发渲染
React 18 引入的并发特性让 Suspense 更加强大。
useTransition 优化列表过滤
TypeScript
import { useState, useTransition, Suspense } from 'react';
function FilterableList({ items }: { items: Item[] }) {
const [filter, setFilter] = useState('');
const [isPending, startTransition] = useTransition();
const filteredItems = items.filter(item =>
item.name.toLowerCase().includes(filter.toLowerCase())
);
return (
<div>
<input
type="text"
value={filter}
onChange={(e) => {
// 紧急更新:立即更新输入框
setFilter(e.target.value);
// 非紧急更新:延迟过滤
startTransition(() => {
// 过滤操作
});
}}
/>
<Suspense fallback={<div>加载中...</div>}>
{isPending && <div className="skeleton">...</div>}
<List items={filteredItems} />
</Suspense>
</div>
);
}
useDeferredValue 延迟昂贵计算
TypeScript
import { useState, useDeferredValue, Suspense } from 'react';
function SearchResults({ query }: { query: string }) {
// 延迟更新搜索结果
const deferredQuery = useDeferredValue(query);
const results = useMemo(() =>
searchProducts(deferredQuery),
[deferredQuery]
);
return (
<Suspense fallback={<div>搜索中...</div>}>
<ResultList results={results} />
</Suspense>
);
}
Suspense List
TypeScript
import { Suspense } from 'react';
// React 18 不再需要 SuspenseList
// 直接嵌套 Suspense 即可
function UserProfile() {
return (
<div>
<Suspense fallback={<HeaderSkeleton />}>
<Header />
</Suspense>
<Suspense fallback={<ContentSkeleton />}>
<Content />
</Suspense>
<Suspense fallback={<CommentsSkeleton />}>
<Comments />
</Suspense>
</div>
);
}
并发渲染的优势
- 可中断渲染:高优先级更新可以打断低优先级渲染
- 保持响应:UI 始终响应用户交互
- 优化体验:优先处理用户关心的问题
Suspense 与 Server Components
React Server Components (RSC) 完全依赖 Suspense 处理流式渲染。
TypeScript
// Server Component (服务端组件)
async function UserProfile({ userId }: { userId: string }) {
// 直接 await 数据
const user = await fetchUser(userId);
const posts = await fetchUserPosts(userId);
return (
<div>
<h1>{user.name}</h1>
{/* 嵌套 Suspense 边界 */}
<Suspense fallback={<div>加载文章中...</div>}>
<PostsList posts={posts} />
</Suspense>
</div>
);
}
// 客户端组件
'use client';
import { Suspense } from 'react';
function Page() {
return (
<Suspense fallback={<div>加载用户信息中...</div>}>
<UserProfile userId="1" />
</Suspense>
);
}
Streaming SSR
Suspense 支持 Streaming SSR,服务端可以渐进式发送 HTML。
TypeScript
// Node.js 服务端
import { renderToPipeableStream } from 'react-dom/server';
app.get('/', (req, res) => {
const { pipe } = renderToPipeableStream(
<App />,
{
// shell 先渲染
onShellReady() {
res.statusCode = 200;
res.setHeader('Content-type', 'text/html');
pipe(res);
},
// 所有内容渲染完成
onAllReady() {
// 如果需要等待所有内容再发送
},
// 错误处理
onError(error) {
console.error(error);
}
}
);
});
Streaming SSR 的优势
- 更快的 TTFB: 尽快发送 HTML
- 渐进式渲染: 内容准备好就发送
- 更好的用户体验: 用户看到内容更快
Suspense 最佳实践
- 提供有意义的 fallback
TypeScript
// ❌ 不好
<Suspense fallback={<div>...</div>}>
// ✅ 好
<Suspense fallback={
<div className="skeleton">
<div className="skeleton-avatar" />
<div className="skeleton-text" />
<div className="skeleton-text" />
</div>
}>
- 细粒度 Suspense
TypeScript
// ❌ 不好:整个页面一个 Suspense
<Suspense fallback={<PageSkeleton />}>
<Header />
<Content />
<Comments />
</Suspense>
// ✅ 好:每个部分独立 Suspense
<Suspense fallback={<HeaderSkeleton />}><Header /></Suspense>
<Suspense fallback={<ContentSkeleton />}><Content /></Suspense>
<Suspense fallback={<CommentsSkeleton />}><Comments /></Suspense>
- 总是配合 Error Boundary
TypeScript
<ErrorBoundary fallback={<ErrorUI />}>
<Suspense fallback={<LoadingUI />}>
<AsyncComponent />
</Suspense>
</ErrorBoundary>
-
避免过度使用
- 不是所有异步操作都需要 Suspense。简单的 loading 状态用 useState 即可。
-
缓存资源
- 避免重复请求相同数据,使用缓存机制。
常见问题
Suspense 不工作?
可能的原因:
- 组件没有抛出 Promise
- 使用的数据获取库不支持 Suspense
- Suspense 位置不正确
如何获取加载进度?
Suspense 本身不提供进度信息。可以使用 onLoading prop 或第三方库:
TypeScript
// 使用 startTransition 配合
const [isPending, startTransition] = useTransition();
startTransition(() => {
// 异步操作
});
Suspense 和 useEffect?
useEffect 在 Suspense 之后执行,即使数据还未加载完成。注意可能的竞态条件。