关于 react-suspense
面向 React 开发者的 Suspense 实践参考,涵盖延迟加载、异步界面及边界设计,并整理错误处理与常见问题。
主要功能
- 01延迟加载组件
- 02嵌套 Suspense 边界
- 03错误边界处理
- 04渐进式内容加载
- 05骨架屏加载模式
适用场景
- 01拆分仪表盘与设置页面代码
- 02加载异步用户资料
- 03分阶段显示页面内容
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ claude-dev-suite/ claude-dev-suite --skill react-suspense安装后在 Claude Code 中输入/react-suspense确认已加载。
附带文件advanced.md
详细介绍
React Suspense
完整参考:有关流式 SSR、SuspenseList、自定义 Suspense 支持的 Hook、图片加载、基于路由的代码拆分和测试模式,请参阅 advanced.md。
不适合使用此技能的情况
- 使用 React 17 或更早版本(支持有限)
- 使用类组件
- 构建非 React 应用
- 所有数据都是静态的(没有异步操作)
核心概念
Suspense 让你能够以声明式方式指定等待异步操作时的加载状态:
import { Suspense } from 'react';
function App() {
return (
<Suspense fallback={<LoadingSpinner />}>
<AsyncComponent />
</Suspense>
);
}
使用 React.lazy 拆分代码
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>
);
}
预加载组件
const Dashboard = lazy(() => import('./Dashboard'));
const preloadDashboard = () => import('./Dashboard');
function NavLink() {
return (
<Link
to="/dashboard"
onMouseEnter={preloadDashboard}
onFocus={preloadDashboard}
>
Dashboard
</Link>
);
}
使用 Suspense 获取数据
使用 TanStack Query
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>
);
}
使用 React 19 use() Hook
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>
);
}
嵌套 Suspense 边界
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>
);
}
使用 Suspense 处理错误边界
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>
渐进式加载
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>
);
}
使用 Transition 处理更新
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>
</>
);
}
骨架屏加载模式
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; }
}
`;
反模式
| 反模式 | 为什么不好 | 正确做法 |
|---|---|---|
| 在渲染时创建 Promise | 每次渲染都会创建新 Promise | 在组件外创建 |
| Suspense 边界过多 | 加载过程过于零散 | 将相关内容分组 |
| 边界过少 | 整个应用都会暂停 | 为各个部分添加边界 |
| 没有 ErrorBoundary | 错误会导致应用崩溃 | 在 Suspense 外层包裹 ErrorBoundary |
| 使用通用加载旋转图标 | 用户体验差 | 使用骨架屏加载器 |
快速故障排查
| 问题 | 解决方案 |
|---|---|
| 无限暂停 | 将 Promise 的创建移到组件外 |
| 加载状态闪现 | 显示 fallback 前添加延迟 |
| 瀑布式加载 | 并行获取数据 |
| 滚动位置丢失 | 使用尺寸相同的骨架屏 |
| 错误未被捕获 | 添加 ErrorBoundary 包装器 |
最佳实践
- ✅ 在有意义的 UI 边界处放置 Suspense
- ✅ 使用尺寸与内容匹配的骨架屏加载器
- ✅ 与 ErrorBoundary 结合,实现完整的错误处理
- ✅ 对非紧急更新使用 transitions
- ✅ 根据用户意图(悬停、聚焦)预加载组件
- ❌ 不要在组件内创建 Promise
- ❌ 不要使用过多细粒度边界
参考文档
评论
支持 Markdown