react-suspense
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseReact Suspense
React Suspense
Full Reference: See advanced.md for Streaming SSR, SuspenseList, Custom Suspense-Enabled Hooks, Image Loading, Route-Based Code Splitting, and Testing patterns.
完整参考:如需了解流式 SSR、SuspenseList、自定义支持 Suspense 的 Hooks、图片加载、基于路由的代码分割以及测试模式,请查看 advanced.md。
When NOT to Use This Skill
不适用此技能的场景
- Using React 17 or earlier (limited support)
- Working with class components
- Building non-React applications
- All data is static (no async operations)
- 使用 React 17 或更早版本(支持有限)
- 处理类组件
- 构建非 React 应用
- 所有数据均为静态数据(无异步操作)
Core Concept
核心概念
Suspense lets you declaratively specify loading states while waiting for async operations:
tsx
import { Suspense } from 'react';
function App() {
return (
<Suspense fallback={<LoadingSpinner />}>
<AsyncComponent />
</Suspense>
);
}Suspense 允许你在等待异步操作时,声明式地指定加载状态:
tsx
import { Suspense } from 'react';
function App() {
return (
<Suspense fallback={<LoadingSpinner />}>
<AsyncComponent />
</Suspense>
);
}Code Splitting with React.lazy
结合 React.lazy 实现代码分割
tsx
import { lazy, Suspense } from 'react';
const Dashboard = lazy(() => import('./Dashboard'));
const Settings = lazy(() => import('./Settings'));
function App() {
const [view, setView] = useState('dashboard');
return (
<div>
<nav>
<button onClick={() => setView('dashboard')}>Dashboard</button>
<button onClick={() => setView('settings')}>Settings</button>
</nav>
<Suspense fallback={<PageSkeleton />}>
{view === 'dashboard' && <Dashboard />}
{view === 'settings' && <Settings />}
</Suspense>
</div>
);
}tsx
import { lazy, Suspense } from 'react';
const Dashboard = lazy(() => import('./Dashboard'));
const Settings = lazy(() => import('./Settings'));
function App() {
const [view, setView] = useState('dashboard');
return (
<div>
<nav>
<button onClick={() => setView('dashboard')}>Dashboard</button>
<button onClick={() => setView('settings')}>Settings</button>
</nav>
<Suspense fallback={<PageSkeleton />}>
{view === 'dashboard' && <Dashboard />}
{view === 'settings' && <Settings />}
</Suspense>
</div>
);
}Preloading Components
预加载组件
tsx
const Dashboard = lazy(() => import('./Dashboard'));
const preloadDashboard = () => import('./Dashboard');
function NavLink() {
return (
<Link
to="/dashboard"
onMouseEnter={preloadDashboard}
onFocus={preloadDashboard}
>
Dashboard
</Link>
);
}tsx
const Dashboard = lazy(() => import('./Dashboard'));
const preloadDashboard = () => import('./Dashboard');
function NavLink() {
return (
<Link
to="/dashboard"
onMouseEnter={preloadDashboard}
onFocus={preloadDashboard}
>
Dashboard
</Link>
);
}Data Fetching with Suspense
结合 Suspense 实现数据获取
Using TanStack Query
使用 TanStack Query
tsx
import { useSuspenseQuery } from '@tanstack/react-query';
function UserProfile({ userId }: { userId: string }) {
const { data: user } = useSuspenseQuery({
queryKey: ['user', userId],
queryFn: () => fetchUser(userId),
});
return <h1>{user.name}</h1>;
}
function UserPage({ userId }: { userId: string }) {
return (
<Suspense fallback={<UserSkeleton />}>
<UserProfile userId={userId} />
</Suspense>
);
}tsx
import { useSuspenseQuery } from '@tanstack/react-query';
function UserProfile({ userId }: { userId: string }) {
const { data: user } = useSuspenseQuery({
queryKey: ['user', userId],
queryFn: () => fetchUser(userId),
});
return <h1>{user.name}</h1>;
}
function UserPage({ userId }: { userId: string }) {
return (
<Suspense fallback={<UserSkeleton />}>
<UserProfile userId={userId} />
</Suspense>
);
}Using React 19 use() Hook
使用 React 19 use() Hook
tsx
import { use, Suspense } from 'react';
function UserProfile({ userPromise }: { userPromise: Promise<User> }) {
const user = use(userPromise);
return <h1>{user.name}</h1>;
}
function UserPage({ userId }: { userId: string }) {
const [userPromise] = useState(() => fetchUser(userId));
return (
<Suspense fallback={<UserSkeleton />}>
<UserProfile userPromise={userPromise} />
</Suspense>
);
}tsx
import { use, Suspense } from 'react';
function UserProfile({ userPromise }: { userPromise: Promise<User> }) {
const user = use(userPromise);
return <h1>{user.name}</h1>;
}
function UserPage({ userId }: { userId: string }) {
const [userPromise] = useState(() => fetchUser(userId));
return (
<Suspense fallback={<UserSkeleton />}>
<UserProfile userPromise={userPromise} />
</Suspense>
);
}Nested Suspense Boundaries
嵌套 Suspense 边界
tsx
function Dashboard() {
return (
<div className="dashboard">
<Suspense fallback={<HeaderSkeleton />}>
<Header />
</Suspense>
<main>
<Suspense fallback={<StatsSkeleton />}>
<Stats />
</Suspense>
<Suspense fallback={<ChartsSkeleton />}>
<Charts />
</Suspense>
<Suspense fallback={<TableSkeleton />}>
<DataTable />
</Suspense>
</main>
</div>
);
}tsx
function Dashboard() {
return (
<div className="dashboard">
<Suspense fallback={<HeaderSkeleton />}>
<Header />
</Suspense>
<main>
<Suspense fallback={<StatsSkeleton />}>
<Stats />
</Suspense>
<Suspense fallback={<ChartsSkeleton />}>
<Charts />
</Suspense>
<Suspense fallback={<TableSkeleton />}>
<DataTable />
</Suspense>
</main>
</div>
);
}Error Boundaries with Suspense
结合 Suspense 使用错误边界
tsx
import { ErrorBoundary, FallbackProps } from 'react-error-boundary';
function ErrorFallback({ error, resetErrorBoundary }: FallbackProps) {
return (
<div>
<h2>Something went wrong</h2>
<pre>{error.message}</pre>
<button onClick={resetErrorBoundary}>Try again</button>
</div>
);
}
// Reusable wrapper
function AsyncBoundary({
children,
fallback,
errorFallback,
}: {
children: React.ReactNode;
fallback: React.ReactNode;
errorFallback: React.ComponentType<FallbackProps>;
}) {
return (
<ErrorBoundary FallbackComponent={errorFallback}>
<Suspense fallback={fallback}>{children}</Suspense>
</ErrorBoundary>
);
}
// Usage
<AsyncBoundary
fallback={<LoadingSpinner />}
errorFallback={ErrorFallback}
>
<AsyncComponent />
</AsyncBoundary>tsx
import { ErrorBoundary, FallbackProps } from 'react-error-boundary';
function ErrorFallback({ error, resetErrorBoundary }: FallbackProps) {
return (
<div>
<h2>出现错误</h2>
<pre>{error.message}</pre>
<button onClick={resetErrorBoundary}>重试</button>
</div>
);
}
// 可复用的包装组件
function AsyncBoundary({
children,
fallback,
errorFallback,
}: {
children: React.ReactNode;
fallback: React.ReactNode;
errorFallback: React.ComponentType<FallbackProps>;
}) {
return (
<ErrorBoundary FallbackComponent={errorFallback}>
<Suspense fallback={fallback}>{children}</Suspense>
</ErrorBoundary>
);
}
// 使用示例
<AsyncBoundary
fallback={<LoadingSpinner />}
errorFallback={ErrorFallback}
>
<AsyncComponent />
</AsyncBoundary>Progressive Loading
渐进式加载
tsx
function ArticlePage({ articleId }: { articleId: string }) {
return (
<article>
{/* Critical content loads first */}
<Suspense fallback={<TitleSkeleton />}>
<ArticleTitle articleId={articleId} />
</Suspense>
{/* Content loads next */}
<Suspense fallback={<ContentSkeleton />}>
<ArticleContent articleId={articleId} />
</Suspense>
{/* Less critical - loads last */}
<Suspense fallback={<CommentsSkeleton />}>
<Comments articleId={articleId} />
</Suspense>
</article>
);
}tsx
function ArticlePage({ articleId }: { articleId: string }) {
return (
<article>
{/* 关键内容优先加载 */}
<Suspense fallback={<TitleSkeleton />}>
<ArticleTitle articleId={articleId} />
</Suspense>
{/* 内容随后加载 */}
<Suspense fallback={<ContentSkeleton />}>
<ArticleContent articleId={articleId} />
</Suspense>
{/* 次要内容最后加载 */}
<Suspense fallback={<CommentsSkeleton />}>
<Comments articleId={articleId} />
</Suspense>
</article>
);
}With Transition for Updates
结合 Transition 处理更新
tsx
import { useState, useTransition, Suspense } from 'react';
function TabContainer() {
const [tab, setTab] = useState('about');
const [isPending, startTransition] = useTransition();
function selectTab(nextTab: string) {
startTransition(() => setTab(nextTab));
}
return (
<>
<TabButtons selectedTab={tab} onSelect={selectTab} />
<div className={isPending ? 'opacity-50' : ''}>
<Suspense fallback={<TabSkeleton />}>
{tab === 'about' && <About />}
{tab === 'posts' && <Posts />}
</Suspense>
</div>
</>
);
}tsx
import { useState, useTransition, Suspense } from 'react';
function TabContainer() {
const [tab, setTab] = useState('about');
const [isPending, startTransition] = useTransition();
function selectTab(nextTab: string) {
startTransition(() => setTab(nextTab));
}
return (
<>
<TabButtons selectedTab={tab} onSelect={selectTab} />
<div className={isPending ? 'opacity-50' : ''}>
<Suspense fallback={<TabSkeleton />}>
{tab === 'about' && <About />}
{tab === 'posts' && <Posts />}
</Suspense>
</div>
</>
);
}Skeleton Loading Patterns
骨架屏加载模式
tsx
function UserCardSkeleton() {
return (
<div className="user-card">
<div className="skeleton skeleton-avatar" />
<div className="skeleton skeleton-text" style={{ width: '60%' }} />
<div className="skeleton skeleton-text" style={{ width: '40%' }} />
</div>
);
}
// CSS
const skeletonStyles = `
.skeleton {
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
background-size: 200% 100%;
animation: skeleton-loading 1.5s infinite;
border-radius: 4px;
}
@keyframes skeleton-loading {
0% { background-position: 200% 0; }
100% { background-position: -200% 0; }
}
`;tsx
function UserCardSkeleton() {
return (
<div className="user-card">
<div className="skeleton skeleton-avatar" />
<div className="skeleton skeleton-text" style={{ width: '60%' }} />
<div className="skeleton skeleton-text" style={{ width: '40%' }} />
</div>
);
}
// CSS 样式
const skeletonStyles = `
.skeleton {
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
background-size: 200% 100%;
animation: skeleton-loading 1.5s infinite;
border-radius: 4px;
}
@keyframes skeleton-loading {
0% { background-position: 200% 0; }
100% { background-position: -200% 0; }
}
`;Anti-Patterns
反模式
| Anti-Pattern | Why It's Bad | Correct Approach |
|---|---|---|
| Creating promises in render | New promise every render | Create outside component |
| Too many Suspense boundaries | Over-fragmented loading | Group related content |
| Too few boundaries | Entire app suspends | Add boundaries per section |
| No ErrorBoundary | Errors crash app | Wrap Suspense in ErrorBoundary |
| Generic loading spinners | Poor UX | Use skeleton loaders |
| 反模式 | 问题所在 | 正确做法 |
|---|---|---|
| 在渲染函数中创建 Promise | 每次渲染都会生成新的 Promise | 在组件外部创建 Promise |
| 过多的 Suspense 边界 | 加载状态过于碎片化 | 对相关内容进行分组 |
| 过少的边界 | 整个应用都会进入挂起状态 | 为每个区块添加边界 |
| 未使用 ErrorBoundary | 错误会导致应用崩溃 | 将 Suspense 包裹在 ErrorBoundary 中 |
| 通用加载动画 | 用户体验不佳 | 使用骨架屏加载器 |
Quick Troubleshooting
快速故障排除
| Issue | Solution |
|---|---|
| Infinite suspending | Move promise creation outside component |
| Flash of loading state | Add delay before showing fallback |
| Waterfall loading | Fetch data in parallel |
| Lost scroll position | Use skeletons with same dimensions |
| Error not caught | Add ErrorBoundary wrapper |
| 问题 | 解决方案 |
|---|---|
| 无限挂起 | 将 Promise 创建移至组件外部 |
| 加载状态闪烁 | 添加延迟后再显示备用UI |
| 瀑布式加载 | 并行获取数据 |
| 滚动位置丢失 | 使用与内容尺寸一致的骨架屏 |
| 错误未被捕获 | 添加 ErrorBoundary 包装器 |
Best Practices
最佳实践
- ✅ Place Suspense at meaningful UI boundaries
- ✅ Use skeleton loaders matching content dimensions
- ✅ Combine with ErrorBoundary for complete error handling
- ✅ Use transitions for non-urgent updates
- ✅ Preload components on user intent (hover, focus)
- ❌ Don't create Promises inside components
- ❌ Don't use too many granular boundaries
- ✅ 将 Suspense 放置在有意义的UI边界处
- ✅ 使用与内容尺寸匹配的骨架屏加载器
- ✅ 结合 ErrorBoundary 实现完整的错误处理
- ✅ 使用 transitions 处理非紧急更新
- ✅ 在用户触发操作前(悬停、聚焦)预加载组件
- ❌ 不要在组件内部创建 Promises
- ❌ 不要使用过多粒度极细的边界