|$ 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
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
| 1 | import { lazy, Suspense, useState } from "react"; |
| 2 | |
| 3 | // lazy() creates a component that is loaded on demand |
| 4 | const HeavyChart = lazy(() => import("./HeavyChart")); |
| 5 | const AdminPanel = lazy(() => import("./AdminPanel")); |
| 6 | const MarkdownEditor = lazy(() => import("./MarkdownEditor")); |
| 7 | |
| 8 | // Wrap lazy components in Suspense |
| 9 | function 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 |
| 36 | const { 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 |
| 46 | const Home = lazy(() => import("./pages/Home")); |
| 47 | const About = lazy(() => import("./pages/About")); |
| 48 | const Contact = lazy(() => import("./pages/Contact")); |
| 49 | |
| 50 | function 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 |
| 63 | const LazyComponent = lazy(() => import("./LazyComponent")); |
| 64 | |
| 65 | function 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 | |
| 3 | function 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 |
| 32 | function 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 |
| 53 | function Skeleton({ className }: { className?: string }) { |
| 54 | return ( |
| 55 | <div |
| 56 | className={`animate-pulse bg-gray-800 rounded ${className ?? ""}`} |
| 57 | /> |
| 58 | ); |
| 59 | } |
| 60 | |
| 61 | function 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 |
| 5 | async 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 |
| 12 | function 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"; |
| 35 | import { use, Suspense } from "react"; |
| 36 | |
| 37 | function fetchPosts(): Promise<Post[]> { |
| 38 | return fetch("/api/posts").then((res) => res.json()); |
| 39 | } |
| 40 | |
| 41 | const postsPromise = fetchPosts(); |
| 42 | |
| 43 | function 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 | |
| 56 | function PostsPage() { |
| 57 | return ( |
| 58 | <Suspense fallback={<p>Loading posts...</p>}> |
| 59 | <PostList /> |
| 60 | </Suspense> |
| 61 | ); |
| 62 | } |
| 63 | |
| 64 | // Parallel data fetching — independent Suspense boundaries |
| 65 | function 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 | |
| 3 | import { use, Suspense, useState } from "react"; |
| 4 | |
| 5 | // use() with a promise — suspends until resolved |
| 6 | async 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 |
| 12 | function createUserPromise(id: string) { |
| 13 | return fetchUser(id); |
| 14 | } |
| 15 | |
| 16 | function 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 | |
| 27 | function 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 |
| 37 | function 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 |
| 52 | function 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 |
| 67 | function 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 |
| 5 | export 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 |
| 27 | export default function DashboardLoading() { |
| 28 | return <DashboardSkeleton />; |
| 29 | } |
| 30 | |
| 31 | // Nested Suspense for progressive enhancement |
| 32 | // app/dashboard/layout.tsx |
| 33 | export 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 |
| 49 | function 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