引言
在现代前端应用中,加载体验至关重要。传统的 loading 状态管理往往会导致代码分散、闪烁等问题。React 18 引入的 Suspense 组件提供了一种声明式、可组合的解决方案。本文将带你从零开始,通过三个实战案例,掌握 Suspense 的核心用法和最佳实践。
什么是 Suspense?
Suspense 是 React 内置组件,用于“等待”某些异步操作(如代码加载、数据获取)完成,并在此期间显示 fallback 内容。其核心思想是:让组件声明其依赖的异步资源,由 React 协调加载状态。
传统方式的痛点
// 传统方式:手动管理 loading
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
setLoading(true);
fetchUser(userId).then(data => {
setUser(data);
setLoading(false);
});
}, [userId]);
if (loading) return <Spinner />;
return <div>{user.name}</div>;
}
问题:
- 每个组件都需要重复写 loading 逻辑
- 多个组件同时加载时,loading 状态难以协调
- 容易出现闪烁(加载瞬间闪过 Spinner)
实战一:懒加载组件(React.lazy + Suspense)
这是 Suspense 最基础的用法:按需加载组件,减少首屏体积。
import React, { Suspense } from 'react';
const HeavyComponent = React.lazy(() => import('./HeavyComponent'));
function App() {
return (
<div>
<h1>My App</h1>
<Suspense fallback={<div>Loading component...</div>}>
<HeavyComponent />
</Suspense>
</div>
);
}
注意:React.lazy 只支持默认导出。如果组件需要命名导出,可以创建一个中间模块。
进阶:错误边界
懒加载可能因网络失败而报错,需要配合错误边界使用。
import React from 'react';
class ErrorBoundary extends React.Component {
state = { hasError: false };
static getDerivedStateFromError() {
return { hasError: true };
}
render() {
if (this.state.hasError) {
return <div>Something went wrong.</div>;
}
return this.props.children;
}
}
// 使用
<ErrorBoundary>
<Suspense fallback={<div>Loading...</div>}>
<HeavyComponent />
</Suspense>
</ErrorBoundary>
实战二:使用 Suspense 获取数据(React-Query)
React Query 等库支持 Suspense 模式,可以优雅地处理数据加载。
安装依赖
npm install @tanstack/react-query
配置 QueryClient 启用 Suspense
import { QueryClient, QueryClientProvider, useSuspenseQuery } from '@tanstack/react-query';
const queryClient = new QueryClient({
defaultOptions: {
queries: {
suspense: true, // 全局启用 Suspense 模式
},
},
});
使用 useSuspenseQuery
function UserProfile({ userId }) {
const { data: user } = useSuspenseQuery({
queryKey: ['user', userId],
queryFn: () => fetch(`/api/users/${userId}`).then(res => res.json()),
});
return <div>{user.name}</div>;
}
// 父组件
function App() {
return (
<QueryClientProvider client={queryClient}>
<Suspense fallback={<div>Loading user...</div>}>
<UserProfile userId={1} />
</Suspense>
</QueryClientProvider>
);
}
优势:
- 无需手动管理 loading 状态
- React Query 自动处理缓存、重试、过期等
- 多个 Suspense 组件可以嵌套,形成加载顺序
实战三:嵌套 Suspense 与过渡效果
当页面有多个异步区域时,可以使用嵌套 Suspense 实现细粒度加载。
import { Suspense } from 'react';
function Page() {
return (
<div>
<h1>Dashboard</h1>
<Suspense fallback={<div>Loading header...</div>}>
<Header />
<Suspense fallback={<div>Loading main content...</div>}>
<MainContent />
<Suspense fallback={<div>Loading footer...</div>}>
<Footer />
</Suspense>
</Suspense>
</Suspense>
</div>
);
}
注意:Suspense 的边界会“冒泡”,如果子 Suspense 没有捕获到异步操作,会向上寻找最近的 Suspense。
使用 startTransition 避免闪烁
在切换路由或数据时,如果新内容很快加载完成,直接显示 fallback 会造成闪烁。React 18 提供了 startTransition 来延迟 fallback 的显示。
import { useTransition, useState } from 'react';
function App() {
const [tab, setTab] = useState('home');
const [isPending, startTransition] = useTransition();
function switchTab(newTab) {
startTransition(() => {
setTab(newTab);
});
}
return (
<div>
<button onClick={() => switchTab('home')}>Home</button>
<button onClick={() => switchTab('about')}>About</button>
<Suspense fallback={<div>Loading...</div>}>
{tab === 'home' ? <Home /> : <About />}
</Suspense>
</div>
);
}
原理:startTransition 标记的更新被视为“非紧急”,React 会延迟显示 fallback 直到超时(默认 5 秒),如果在此期间更新完成,则直接显示新内容,避免闪烁。
常见坑与最佳实践
1. 不要将 Suspense 放在频繁变化的父组件中
// ❌ 错误:每次 count 变化都会重新挂载 Suspense
function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<button onClick={() => setCount(c => c + 1)}>{count}</button>
<Suspense fallback={<div>Loading...</div>}>
<SlowComponent />
</Suspense>
</div>
);
}
原因:Suspense 的 key 不变,但每次重渲染都会重新创建 fallback。应确保 Suspense 的层级稳定。
2. 避免在 Suspense 内部使用 useEffect 加载数据
// ❌ 错误:Suspense 无法捕获 useEffect 中的异步操作
function BadComponent() {
useEffect(() => {
fetchData().then(data => setData(data));
}, []);
return <div>{data}</div>;
}
Suspense 只能捕获在渲染期间抛出的 Promise(如 useSuspenseQuery 或 React.lazy)。
3. 合理设置 fallback 的粒度
过度使用 Suspense 会导致大量 fallback 闪烁,建议在页面级别或功能模块级别使用。
总结
通过本文的三个实战,你已经学会了:
- 使用
React.lazy和 Suspense 进行代码分割 - 使用 React Query 的 Suspense 模式获取数据
- 嵌套 Suspense 实现细粒度加载
- 使用
startTransition避免闪烁
Suspense 的未来还包括 Server Components 和 Streaming,值得持续关注。