React Suspense
Suspense 讓你可以在子元件尚未準備好時顯示備用內容(fallback),例如載入中的提示。它是 React 處理非同步載入的核心機制。
基本用法
import { Suspense } from 'react'
function App() {
return (
<Suspense fallback={<p>載入中...</p>}>
<AsyncComponent />
</Suspense>
)
}
當 AsyncComponent 尚未準備好時(例如正在等待資料載入),會顯示 fallback 的內容。
Suspense 的運作原理
當被 Suspense 包裹的子元件「暫停」時,Suspense 會顯示 fallback。子元件可以透過以下方式觸發暫停:
- 使用 use Hook 讀取 Promise
- 使用支援 Suspense 的資料獲取函式庫
- 使用
React.lazy()動態載入元件
搭配 use Hook 載入資料
import { use, Suspense } from 'react'
async function fetchAlbums(artistId) {
const response = await fetch(`/api/artists/${artistId}/albums`)
return response.json()
}
function Albums({ artistId }) {
const albums = use(fetchAlbums(artistId))
return (
<ul>
{albums.map((album) => (
<li key={album.id}>{album.title}</li>
))}
</ul>
)
}
function ArtistPage({ artistId }) {
return (
<div>
<h1>專輯列表</h1>
<Suspense fallback={<p>載入專輯中...</p>}>
<Albums artistId={artistId} />
</Suspense>
</div>
)
}
動態載入元件 (React.lazy)
React.lazy 讓你可以動態載入元件,搭配 Suspense 使用:
import { Suspense, lazy } from 'react'
// 動態載入元件(只有在需要時才會載入)
const HeavyComponent = lazy(() => import('./HeavyComponent'))
const AdminPanel = lazy(() => import('./AdminPanel'))
function App() {
return (
<div>
<h1>我的應用程式</h1>
<Suspense fallback={<p>載入中...</p>}>
<HeavyComponent />
</Suspense>
</div>
)
}
這種技術稱為程式碼分割(Code Splitting),可以減少初始載入的 JavaScript 大小。
巢狀 Suspense
你可以使用多層 Suspense 來控制不同區塊的載入狀態:
function App() {
return (
<Suspense fallback={<p>載入頁面...</p>}>
<Header />
<main>
<Suspense fallback={<p>載入使用者資訊...</p>}>
<UserProfile />
</Suspense>
<Suspense fallback={<p>載入文章...</p>}>
<Articles />
</Suspense>
</main>
</Suspense>
)
}
好處:
- 不同區塊可以獨立載入
- 先載入完成的區塊會先顯示
- 使用者可以更快看到部分內容
Skeleton 載入畫面
比起單純的「載入中...」文字,使用 Skeleton(骨架屏)可以提供更好的使用者體驗:
function ArticleSkeleton() {
return (
<div className="article-skeleton">
<div className="skeleton-title" />
<div className="skeleton-line" />
<div className="skeleton-line" />
<div className="skeleton-line short" />
</div>
)
}
function ArticleList() {
return (
<Suspense fallback={<ArticleSkeleton />}>
<Articles />
</Suspense>
)
}
CSS 範例:
.skeleton-title,
.skeleton-line {
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
background-size: 200% 100%;
animation: shimmer 1.5s infinite;
border-radius: 4px;
}
.skeleton-title {
height: 24px;
width: 60%;
margin-bottom: 16px;
}
.skeleton-line {
height: 16px;
margin-bottom: 8px;
}
.skeleton-line.short {
width: 40%;
}
@keyframes shimmer {
0% {
background-position: -200% 0;
}
100% {
background-position: 200% 0;
}
}
平行載入 vs 串列載入
平行載入(推薦)
多個資源同時開始載入:
function UserPage({ userId }) {
// 同時開始載入(平行)
const userPromise = fetchUser(userId)
const postsPromise = fetchPosts(userId)
return (
<div>
<Suspense fallback={<p>載入使用者...</p>}>
<UserInfo userPromise={userPromise} />
</Suspense>
<Suspense fallback={<p>載入文章...</p>}>
<UserPosts postsPromise={postsPromise} />
</Suspense>
</div>
)
}
串列載入(瀑布式)
一個載入完成後才開始下一個,應該避免:
// ❌ 不好:串列載入(瀑布式)
function UserPosts({ userId }) {
// 要等 UserInfo 載入完成後才會開始載入文章
const posts = use(fetchPosts(userId))
// ...
}
function UserInfo({ userId }) {
const user = use(fetchUser(userId))
return (
<div>
<h1>{user.name}</h1>
{/* 等 user 載入完成後才渲染 UserPosts */}
<Suspense fallback={<p>載入文章...</p>}>
<UserPosts userId={userId} />
</Suspense>
</div>
)
}
搭配錯誤邊界
使用 Error Boundary 處理載入失敗的情況:
import { Suspense } from 'react'
import { ErrorBoundary } from 'react-error-boundary'
function ErrorFallback({ error, resetErrorBoundary }) {
return (
<div className="error">
<p>發生錯誤:{error.message}</p>
<button onClick={resetErrorBoundary}>重試</button>
</div>
)
}
function App() {
return (
<ErrorBoundary FallbackComponent={ErrorFallback}>
<Suspense fallback={<p>載入中...</p>}>
<DataComponent />
</Suspense>
</ErrorBoundary>
)
}
實際範例:儀表板頁面
import { Suspense, lazy } from 'react'
// 動態載入圖表元件
const SalesChart = lazy(() => import('./SalesChart'))
const TrafficChart = lazy(() => import('./TrafficChart'))
function DashboardSkeleton() {
return (
<div className="dashboard-skeleton">
<div className="skeleton-card" />
<div className="skeleton-card" />
<div className="skeleton-chart" />
</div>
)
}
function StatsSkeleton() {
return (
<div className="stats-skeleton">
<div className="skeleton-stat" />
<div className="skeleton-stat" />
<div className="skeleton-stat" />
</div>
)
}
function Dashboard() {
const statsPromise = fetchDashboardStats()
const salesPromise = fetchSalesData()
const trafficPromise = fetchTrafficData()
return (
<div className="dashboard">
<h1>儀表板</h1>
{/* 統計數據 */}
<Suspense fallback={<StatsSkeleton />}>
<StatsCards statsPromise={statsPromise} />
</Suspense>
<div className="charts-grid">
{/* 銷售圖表 */}
<Suspense fallback={<p>載入銷售圖表...</p>}>
<SalesChart dataPromise={salesPromise} />
</Suspense>
{/* 流量圖表 */}
<Suspense fallback={<p>載入流量圖表...</p>}>
<TrafficChart dataPromise={trafficPromise} />
</Suspense>
</div>
</div>
)
}