React Suspense 用法详解
一、Suspense 是什么
Suspense 用于在子组件加载完成之前显示备用内容,例如:
- 加载动画
- 骨架屏
- 加载提示
- 懒加载组件
- Suspense 数据请求
基本写法:
jsx
import { Suspense } from 'react'
function App() {
return (
<Suspense fallback={<p>加载中...</p>}>
<UserInfo />
</Suspense>
)
}
当 UserInfo 还没有准备好时,React 会显示:
jsx
<p>加载中...</p>
准备完成后,React 会自动显示 UserInfo。
官方文档:React Suspense
二、Suspense 的基本语法
jsx
<Suspense fallback={备用内容}>
{需要等待的组件}
</Suspense>
例如:
jsx
<Suspense fallback={<Loading />}>
<Profile />
</Suspense>
其中:
children:真正要显示的内容fallback:加载期间显示的内容
fallback 可以是任意 React 节点:
jsx
<Suspense fallback={<div>加载中...</div>}>
<App />
</Suspense>
jsx
<Suspense fallback={<Loading />}>
<App />
</Suspense>
jsx
<Suspense fallback={null}>
<App />
</Suspense>
三、Suspense 配合 lazy 实现组件懒加载
这是 Suspense 最常见的用法。
1. 创建懒加载组件
jsx
import { lazy, Suspense } from 'react'
const About = lazy(() => import('./About.jsx'))
function App() {
return (
<Suspense fallback={<p>About 加载中...</p>}>
<About />
</Suspense>
)
}
export default App
执行过程:
text
第一次渲染 About
↓
浏览器开始加载 About.js
↓
显示 fallback
↓
About.js 加载完成
↓
显示 About 组件
lazy 会延迟加载组件代码,直到组件第一次真正被渲染。React lazy 官方文档
2. 被懒加载的组件必须使用默认导出
About.jsx:
jsx
export default function About() {
return <h1>关于我们</h1>
}
如果使用普通导出:
jsx
export function About() {
return <h1>关于我们</h1>
}
需要这样写:
jsx
const About = lazy(() =>
import('./About.jsx').then(module => ({
default: module.About
}))
)
3. lazy 必须写在组件外部
错误写法:
jsx
function App() {
const About = lazy(() => import('./About.jsx'))
return (
<Suspense fallback={<p>加载中...</p>}>
<About />
</Suspense>
)
}
这样可能导致组件每次重新渲染时都被重新创建,内部状态被重置。
正确写法:
jsx
const About = lazy(() => import('./About.jsx'))
function App() {
return (
<Suspense fallback={<p>加载中...</p>}>
<About />
</Suspense>
)
}
四、Suspense 配合路由实现懒加载
使用 React Router 时,可以对页面进行懒加载:
jsx
import { lazy, Suspense } from 'react'
const Home = lazy(() => import('./pages/Home.jsx'))
const User = lazy(() => import('./pages/User.jsx'))
function App() {
return (
<Suspense fallback={<p>页面加载中...</p>}>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/user" element={<User />} />
</Routes>
</Suspense>
)
}
这样首页和用户页的代码不会一次性全部加载,而是在访问对应路由时再加载。
也可以针对不同页面设置不同的加载提示:
jsx
<Route
path="/user"
element={
<Suspense fallback={<p>用户页面加载中...</p>}>
<User />
</Suspense>
}
/>
五、嵌套 Suspense
多个组件可以使用多个 Suspense 边界:
jsx
function App() {
return (
<Suspense fallback={<p>页面加载中...</p>}>
<Profile />
<Suspense fallback={<p>文章加载中...</p>}>
<ArticleList />
</Suspense>
</Suspense>
)
}
加载过程:
text
Profile 未加载完成
↓
显示"页面加载中"
Profile 加载完成
↓
显示 Profile
↓
ArticleList 未加载完成
↓
显示"文章加载中"
ArticleList 加载完成
↓
显示文章列表
嵌套 Suspense 可以让页面逐步展示,而不是所有内容加载完成后一次性显示。
六、一个 Suspense 边界会影响哪些内容
例如:
jsx
<Suspense fallback={<Loading />}>
<Header />
<UserInfo />
<ArticleList />
</Suspense>
如果 UserInfo 正在加载,整个 Suspense 区域都可能显示:
jsx
<Loading />
也就是说,Header、UserInfo 和 ArticleList 会被当成一个整体处理。
如果只希望文章区域显示加载状态,可以缩小边界:
jsx
<Header />
<Suspense fallback={<p>用户信息加载中...</p>}>
<UserInfo />
</Suspense>
<Suspense fallback={<p>文章加载中...</p>}>
<ArticleList />
</Suspense>
Suspense 边界不应该随意包裹每一个小组件,而应该根据页面的加载体验来划分。
七、Suspense 不能直接监听 useEffect 中的请求
下面这种写法不会触发 Suspense:
jsx
function UserList() {
const [loading, setLoading] = useState(true)
const [users, setUsers] = useState([])
useEffect(() => {
fetch('/api/users')
.then(response => response.json())
.then(data => {
setUsers(data)
setLoading(false)
})
}, [])
return <div>用户列表</div>
}
即使外层有:
jsx
<Suspense fallback={<p>加载中...</p>}>
<UserList />
</Suspense>
也不会自动显示 Suspense 的 fallback。
原因是:
Suspense 不会自动检测
useEffect中发起的请求。
官方文档也明确说明,普通 useEffect 中的数据请求不会激活 Suspense。React Suspense 官方说明
普通项目中可以继续使用:
jsx
if (loading) {
return <p>加载中...</p>
}
如果要让数据请求触发 Suspense,需要使用:
- 支持 Suspense 的框架
- 支持 Suspense 的数据请求库
- React 19 的
use(Promise) - 自己实现 Promise 缓存
八、React 19 中使用 use 和 Suspense 加载数据
React 19 提供了 use API,可以读取 Promise。
jsx
import { Suspense, use } from 'react'
function UserInfo({ userPromise }) {
const user = use(userPromise)
return (
<div>
用户名:{user.name}
</div>
)
}
function App() {
const userPromise = fetch('/api/user')
.then(response => response.json())
return (
<Suspense fallback={<p>用户信息加载中...</p>}>
<UserInfo userPromise={userPromise} />
</Suspense>
)
}
export default App
当 Promise 处于 pending 状态时,Suspense 会显示 fallback。
Promise 完成后,Suspense 会显示真正的内容。
use 读取 Promise 时,必须保证 Promise 可以复用,不能在每次渲染时都创建一个全新的 Promise。React use 官方文档
不推荐的写法
jsx
function UserInfo() {
const user = use(
fetch('/api/user').then(response => response.json())
)
return <div>{user.name}</div>
}
每次渲染都可能重新创建 Promise,导致重复请求或持续显示 fallback。
可以使用简单的缓存:
jsx
const cache = new Map()
function fetchUser(url) {
if (!cache.has(url)) {
const promise = fetch(url).then(response => {
if (!response.ok) {
throw new Error('请求失败')
}
return response.json()
})
cache.set(url, promise)
}
return cache.get(url)
}
使用:
jsx
function UserInfo() {
const user = use(fetchUser('/api/user'))
return <div>用户名:{user.name}</div>
}
实际开发中,更推荐使用框架或数据请求库提供的缓存方案,而不是自己实现完整的数据缓存。
九、Suspense 配合错误边界
Suspense 主要处理"等待加载",不负责处理请求错误。
如果 Promise 被拒绝,需要使用 Error Boundary 处理错误。
jsx
import { Component, Suspense } from 'react'
class ErrorBoundary extends Component {
constructor(props) {
super(props)
this.state = {
hasError: false
}
}
static getDerivedStateFromError() {
return {
hasError: true
}
}
render() {
if (this.state.hasError) {
return <p>加载失败,请稍后重试</p>
}
return this.props.children
}
}
使用:
jsx
<ErrorBoundary>
<Suspense fallback={<p>加载中...</p>}>
<UserInfo />
</Suspense>
</ErrorBoundary>
职责可以这样理解:
text
Suspense 处理等待
ErrorBoundary 处理错误
十、更新时避免页面闪回 Loading
当已经显示的内容因为更新再次触发 Suspense 时,页面可能重新显示 fallback:
text
旧页面
↓
切换新页面
↓
新页面加载中
↓
旧页面被替换成 Loading
这种体验可能比较突兀。
可以使用 startTransition:
jsx
import {
Suspense,
startTransition,
useState
} from 'react'
function App() {
const [page, setPage] = useState('home')
function handleChangePage(nextPage) {
startTransition(() => {
setPage(nextPage)
})
}
return (
<div>
<button onClick={() => handleChangePage('home')}>
首页
</button>
<button onClick={() => handleChangePage('user')}>
用户页
</button>
<Suspense fallback={<p>页面加载中...</p>}>
{page === 'home' ? <Home /> : <User />}
</Suspense>
</div>
)
}
startTransition 会告诉 React:
这是一个不紧急的更新,可以尽量保留当前已经显示的内容。
React 官方也建议使用 startTransition 避免更新时已经展示的内容突然被 fallback 替换。React useTransition 文档
十一、Suspense 和普通 loading 的区别
普通 loading
jsx
function App() {
const [loading, setLoading] = useState(true)
if (loading) {
return <Loading />
}
return <Content />
}
特点:
- 手动管理
loading - 手动管理错误
- 手动管理请求结果
- 写法直观,适合普通请求
Suspense
jsx
<Suspense fallback={<Loading />}>
<Content />
</Suspense>
特点:
- 由父组件统一提供 fallback
- 多个子组件可以共享加载边界
- 可以和
lazy、use、数据请求库结合 - 更适合代码分割和支持 Suspense 的数据加载
十二、常见错误
1. 只使用 Suspense,不使用 lazy 或 Suspense 数据源
jsx
<Suspense fallback={<Loading />}>
<普通组件 />
</Suspense>
如果普通组件没有触发 Suspense,fallback 不会显示。
2. 在组件内部声明 lazy
错误:
jsx
function App() {
const Page = lazy(() => import('./Page.jsx'))
return <Page />
}
正确:
jsx
const Page = lazy(() => import('./Page.jsx'))
function App() {
return <Page />
}
3. 把 Suspense 当成请求错误处理
jsx
<Suspense fallback={<p>请求失败</p>}>
<UserInfo />
</Suspense>
fallback 表示"还在加载",不是"请求失败"。
请求失败应该使用 Error Boundary 或普通错误状态处理。
4. Suspense 边界范围太大
jsx
<Suspense fallback={<Loading />}>
<整个页面 />
</Suspense>
这样只要其中一个区域加载,整个页面都可能显示 Loading。
应该根据页面结构划分边界:
jsx
<Header />
<Suspense fallback={<UserSkeleton />}>
<UserInfo />
</Suspense>
<Suspense fallback={<ListSkeleton />}>
<ArticleList />
</Suspense>
十三、总结
Suspense 最常见的使用方式是:
jsx
import { lazy, Suspense } from 'react'
const Page = lazy(() => import('./Page.jsx'))
function App() {
return (
<Suspense fallback={<p>加载中...</p>}>
<Page />
</Suspense>
)
}
记住以下几点:
text
Suspense 用于显示加载中的备用 UI
lazy 用于懒加载组件代码
useEffect 中的 fetch 不会自动触发 Suspense
use(Promise) 可以配合 Suspense 读取异步数据
ErrorBoundary 用于处理加载错误
startTransition 可以避免页面闪回 fallback
一句话记忆:
Suspense就像一个加载边界:子组件还没准备好时显示fallback,准备好后自动显示真实内容。