|$ curl https://forge-ai.dev/api/markdown?path=docs/react/suspense
$cat docs/react-—-suspense.md
updated Last week·16 min read·published

React — Suspense

ReactIntermediate to Advanced🎯Free Tools
What is Suspense?

Suspense is a React component that lets you display a fallback while its children are loading. It works with asynchronous operations like lazy loading, data fetching, and server components. Suspense lets you declaratively handle loading states instead of managing isLoading flags throughout your code.

Lazy Loading Components
lazy_loading.tsx
TSX
1import { lazy, Suspense, useState } from "react";
2
3// lazy() creates a component that is loaded on demand
4const HeavyChart = lazy(() => import("./HeavyChart"));
5const AdminPanel = lazy(() => import("./AdminPanel"));
6const MarkdownEditor = lazy(() => import("./MarkdownEditor"));
7
8// Wrap lazy components in Suspense
9function Dashboard() {
10 const [showAdmin, setShowAdmin] = useState(false);
11
12 return (
13 <div>
14 <h1>Dashboard</h1>
15
16 {/* HeavyChart loads when Dashboard renders */}
17 <Suspense fallback={<div className="h-64 bg-gray-800 animate-pulse rounded" />}>
18 <HeavyChart data={chartData} />
19 </Suspense>
20
21 {/* AdminPanel loads only when button is clicked */}
22 <button onClick={() => setShowAdmin(true)}>
23 Open Admin
24 </button>
25
26 {showAdmin && (
27 <Suspense fallback={<p>Loading admin panel...</p>}>
28 <AdminPanel />
29 </Suspense>
30 )}
31 </div>
32 );
33}
34
35// Lazy with named exports
36const { UserList, UserDetail } = {
37 UserList: lazy(() =>
38 import("./users").then((mod) => ({ default: mod.UserList }))
39 ),
40 UserDetail: lazy(() =>
41 import("./users").then((mod) => ({ default: mod.UserDetail }))
42 ),
43};
44
45// Route-based code splitting
46const Home = lazy(() => import("./pages/Home"));
47const About = lazy(() => import("./pages/About"));
48const Contact = lazy(() => import("./pages/Contact"));
49
50function App() {
51 return (
52 <Suspense fallback={<PageSkeleton />}>
53 <Routes>
54 <Route path="/" element={<Home />} />
55 <Route path="/about" element={<About />} />
56 <Route path="/contact" element={<Contact />} />
57 </Routes>
58 </Suspense>
59 );
60}
61
62// Error handling with lazy + Suspense
63const LazyComponent = lazy(() => import("./LazyComponent"));
64
65function SafeLazy() {
66 return (
67 <ErrorBoundary fallback={<p>Failed to load component</p>}>
68 <Suspense fallback={<Spinner />}>
69 <LazyComponent />
70 </Suspense>
71 </ErrorBoundary>
72 );
73}

info

React.lazy() creates a loadable component. The imported file must export a default React component. For named exports, use .then() to map to default.
Fallback UI Patterns
fallback.tsx
TSX
1// Multiple Suspense boundaries with different fallbacks
2
3function App() {
4 return (
5 <div className="app">
6 {/* Global fallback for the entire page shell */}
7 <Suspense fallback={<PageSkeleton />}>
8 <Header />
9
10 {/* Granular fallback for slow content */}
11 <main>
12 <Suspense fallback={<SidebarSkeleton />}>
13 <Sidebar />
14 </Suspense>
15
16 <section>
17 <Suspense fallback={<ChartSkeleton />}>
18 <SlowChart />
19 </Suspense>
20
21 <Suspense fallback={<TableSkeleton />}>
22 <DataTable />
23 </Suspense>
24 </section>
25 </main>
26 </Suspense>
27 </div>
28 );
29}
30
31// Progressive loading with nested Suspense
32function ArticlePage() {
33 return (
34 <Suspense fallback={<ArticleSkeleton />}>
35 <article>
36 <Suspense fallback={<TitleSkeleton />}>
37 <ArticleTitle />
38 </Suspense>
39
40 <Suspense fallback={<ContentSkeleton />}>
41 <ArticleContent />
42 </Suspense>
43
44 <Suspense fallback={<CommentsSkeleton />}>
45 <Comments />
46 </Suspense>
47 </article>
48 </Suspense>
49 );
50}
51
52// Skeleton component for consistent loading states
53function Skeleton({ className }: { className?: string }) {
54 return (
55 <div
56 className={`animate-pulse bg-gray-800 rounded ${className ?? ""}`}
57 />
58 );
59}
60
61function PageSkeleton() {
62 return (
63 <div className="p-4 space-y-4">
64 <Skeleton className="h-8 w-64" />
65 <Skeleton className="h-64 w-full" />
66 <div className="grid grid-cols-3 gap-4">
67 <Skeleton className="h-32" />
68 <Skeleton className="h-32" />
69 <Skeleton className="h-32" />
70 </div>
71 </div>
72 );
73}
Suspense for Data Fetching
data_fetching.tsx
TSX
1// Suspense integrates with data fetching through frameworks
2// In Next.js App Router, Server Components use Suspense natively
3
4// Next.js pattern — Server Component with Suspense
5async function UserProfile({ userId }: { userId: string }) {
6 // This will suspend while data is loading
7 const user = await fetchUser(userId);
8 return <div>{user.name}</div>;
9}
10
11// Page with granular Suspense
12function ProfilePage() {
13 return (
14 <div>
15 <h1>Profile</h1>
16
17 {/* Static content renders immediately */}
18 <ProfileHeader />
19
20 {/* User data streams in when ready */}
21 <Suspense fallback={<ProfileSkeleton />}>
22 <UserProfile userId="123" />
23 </Suspense>
24
25 {/* Activity loads independently */}
26 <Suspense fallback={<ActivitySkeleton />}>
27 <UserActivity userId="123" />
28 </Suspense>
29 </div>
30 );
31}
32
33// Client-side data fetching with Suspense (React 19 use hook)
34"use client";
35import { use, Suspense } from "react";
36
37function fetchPosts(): Promise<Post[]> {
38 return fetch("/api/posts").then((res) => res.json());
39}
40
41const postsPromise = fetchPosts();
42
43function PostList() {
44 // use() suspends until the promise resolves
45 const posts = use(postsPromise);
46
47 return (
48 <ul>
49 {posts.map((post) => (
50 <li key={post.id}>{post.title}</li>
51 ))}
52 </ul>
53 );
54}
55
56function PostsPage() {
57 return (
58 <Suspense fallback={<p>Loading posts...</p>}>
59 <PostList />
60 </Suspense>
61 );
62}
63
64// Parallel data fetching — independent Suspense boundaries
65function Dashboard() {
66 return (
67 <div className="grid grid-cols-2 gap-4">
68 <Suspense fallback={<CardSkeleton />}>
69 <RecentOrders />
70 </Suspense>
71
72 <Suspense fallback={<CardSkeleton />}>
73 <TopProducts />
74 </Suspense>
75
76 <Suspense fallback={<CardSkeleton />}>
77 <UserStats />
78 </Suspense>
79
80 <Suspense fallback={<CardSkeleton />}>
81 <RevenueChart />
82 </Suspense>
83 </div>
84 );
85}
The use() Hook
use_hook.tsx
TSX
1// React 19 introduces use() — reads a promise or context in render
2
3import { use, Suspense, useState } from "react";
4
5// use() with a promise — suspends until resolved
6async function fetchUser(id: string) {
7 const res = await fetch(`/api/users/${id}`);
8 return res.json();
9}
10
11// Create promise outside component to avoid re-fetching
12function createUserPromise(id: string) {
13 return fetchUser(id);
14}
15
16function UserProfile({ userId }: { userId: string }) {
17 const user = use(createUserPromise(userId));
18
19 return (
20 <div>
21 <h2>{user.name}</h2>
22 <p>{user.email}</p>
23 </div>
24 );
25}
26
27function UserPage() {
28 return (
29 <Suspense fallback={<p>Loading user...</p>}>
30 <UserProfile userId="123" />
31 </Suspense>
32 );
33}
34
35// use() with context — alternative to useContext
36// Unlike useContext, use() can be called in if/for blocks
37function NotificationBadge() {
38 const [isLoggedIn, setIsLoggedIn] = useState(false);
39
40 // Conditional context reading
41 if (!isLoggedIn) {
42 return <button onClick={() => setIsLoggedIn(true)}>Login</button>;
43 }
44
45 // Can use() conditionally — unlike useContext
46 const notifications = use(NotificationContext);
47
48 return <span>{notifications.length} notifications</span>;
49}
50
51// use() with multiple promises
52function DashboardData() {
53 const user = use(fetchUserPromise);
54 const posts = use(fetchPostsPromise);
55 const stats = use(fetchStatsPromise);
56
57 return (
58 <div>
59 <h1>Welcome, {user.name}</h1>
60 <p>{posts.length} posts</p>
61 <p>{stats.views} views</p>
62 </div>
63 );
64}
65
66// use() enables conditional rendering with async data
67function ConditionalFeature({ showAnalytics }: { showAnalytics: boolean }) {
68 if (!showAnalytics) return null;
69
70 // Only fetches when showAnalytics is true
71 const analytics = use(fetchAnalyticsPromise);
72
73 return <AnalyticsChart data={analytics} />;
74}

info

The use() hook can be called conditionally — unlike useContext. It suspends the component when a promise is pending, allowing Suspense to show a fallback.
Streaming SSR
streaming_ssr.tsx
TSX
1// Next.js App Router streams Server Components automatically
2// Suspense boundaries determine what streams early vs late
3
4// app/dashboard/page.tsx — the shell streams immediately
5export default function DashboardPage() {
6 return (
7 <div>
8 {/* This renders in the initial HTML */}
9 <h1>Dashboard</h1>
10 <nav>...</nav>
11
12 {/* This streams when ready */}
13 <Suspense fallback={<RevenueSkeleton />}>
14 <SlowRevenueChart />
15 </Suspense>
16
17 {/* This streams independently */}
18 <Suspense fallback={<ActivitySkeleton />}>
19 <RecentActivity />
20 </Suspense>
21 </div>
22 );
23}
24
25// loading.tsx acts as a Suspense boundary for the entire route
26// app/dashboard/loading.tsx
27export default function DashboardLoading() {
28 return <DashboardSkeleton />;
29}
30
31// Nested Suspense for progressive enhancement
32// app/dashboard/layout.tsx
33export default function DashboardLayout({ children }: { children: React.ReactNode }) {
34 return (
35 <div className="flex">
36 <aside className="w-64">
37 <Suspense fallback={<NavSkeleton />}>
38 <DashboardNav />
39 </Suspense>
40 </aside>
41 <main className="flex-1">
42 {children}
43 </main>
44 </div>
45 );
46}
47
48// Selective streaming — load expensive components last
49function ProductPage() {
50 return (
51 <div>
52 {/* Renders immediately — title and basic info */}
53 <ProductInfo />
54
55 {/* Streams in — price and add-to-cart */}
56 <Suspense fallback={<PriceSkeleton />}>
57 <ProductPricing />
58 </Suspense>
59
60 {/* Streams last — expensive reviews with comments */}
61 <Suspense fallback={<ReviewsSkeleton />}>
62 <ProductReviews />
63 </Suspense>
64 </div>
65 );
66}
$Blueprint — Engineering Documentation·Section ID: REACT-SPNS·Revision: 1.0