React Suspense enables declarative loading states. Here's how to use it effectively for data fetching and code splitting.
Basic Suspense#
1import { Suspense, lazy } from 'react';
2
3// Code splitting with lazy
4const HeavyComponent = lazy(() => import('./HeavyComponent'));
5
6function App() {
7 return (
8 <Suspense fallback={<LoadingSpinner />}>
9 <HeavyComponent />
10 </Suspense>
11 );
12}
13
14// Multiple lazy components
15const Dashboard = lazy(() => import('./Dashboard'));
16const Analytics = lazy(() => import('./Analytics'));
17const Settings = lazy(() => import('./Settings'));
18
19function App() {
20 return (
21 <Suspense fallback={<PageSkeleton />}>
22 <Routes>
23 <Route path="/dashboard" element={<Dashboard />} />
24 <Route path="/analytics" element={<Analytics />} />
25 <Route path="/settings" element={<Settings />} />
26 </Routes>
27 </Suspense>
28 );
29}Data Fetching with Suspense#
1// Using React Query with Suspense
2import { useSuspenseQuery } from '@tanstack/react-query';
3
4function UserProfile({ userId }: { userId: string }) {
5 const { data: user } = useSuspenseQuery({
6 queryKey: ['user', userId],
7 queryFn: () => fetchUser(userId),
8 });
9
10 // No loading check needed - Suspense handles it
11 return (
12 <div>
13 <h1>{user.name}</h1>
14 <p>{user.email}</p>
15 </div>
16 );
17}
18
19function UserPage({ userId }: { userId: string }) {
20 return (
21 <Suspense fallback={<ProfileSkeleton />}>
22 <UserProfile userId={userId} />
23 </Suspense>
24 );
25}
26
27// Multiple suspending components
28function Dashboard() {
29 return (
30 <div className="dashboard">
31 <Suspense fallback={<StatsSkeleton />}>
32 <Stats />
33 </Suspense>
34
35 <Suspense fallback={<ChartSkeleton />}>
36 <RevenueChart />
37 </Suspense>
38
39 <Suspense fallback={<TableSkeleton />}>
40 <RecentOrders />
41 </Suspense>
42 </div>
43 );
44}Nested Suspense Boundaries#
1// Granular loading states
2function ProductPage({ productId }: { productId: string }) {
3 return (
4 <div className="product-page">
5 {/* Critical content loads first */}
6 <Suspense fallback={<ProductHeaderSkeleton />}>
7 <ProductHeader productId={productId} />
8
9 {/* Secondary content can load independently */}
10 <div className="product-details">
11 <Suspense fallback={<DescriptionSkeleton />}>
12 <ProductDescription productId={productId} />
13 </Suspense>
14
15 <Suspense fallback={<ReviewsSkeleton />}>
16 <ProductReviews productId={productId} />
17 </Suspense>
18 </div>
19 </Suspense>
20
21 {/* Recommendations load last */}
22 <Suspense fallback={<RecommendationsSkeleton />}>
23 <RelatedProducts productId={productId} />
24 </Suspense>
25 </div>
26 );
27}
28
29// Avoid nested loading waterfalls
30// Bad: Each component waits for parent to load
31function BadPattern() {
32 return (
33 <Suspense fallback={<Loading />}>
34 <Parent>
35 <Suspense fallback={<Loading />}>
36 <Child /> {/* Waits for Parent to load */}
37 </Suspense>
38 </Parent>
39 </Suspense>
40 );
41}
42
43// Good: Parallel loading with SuspenseList
44import { SuspenseList } from 'react';
45
46function GoodPattern() {
47 return (
48 <SuspenseList revealOrder="forwards">
49 <Suspense fallback={<HeaderSkeleton />}>
50 <Header />
51 </Suspense>
52 <Suspense fallback={<ContentSkeleton />}>
53 <Content />
54 </Suspense>
55 <Suspense fallback={<FooterSkeleton />}>
56 <Footer />
57 </Suspense>
58 </SuspenseList>
59 );
60}Error Boundaries with Suspense#
1import { Suspense } from 'react';
2import { ErrorBoundary } from 'react-error-boundary';
3
4function ErrorFallback({ error, resetErrorBoundary }) {
5 return (
6 <div role="alert">
7 <p>Something went wrong:</p>
8 <pre>{error.message}</pre>
9 <button onClick={resetErrorBoundary}>Try again</button>
10 </div>
11 );
12}
13
14function DataSection() {
15 return (
16 <ErrorBoundary FallbackComponent={ErrorFallback}>
17 <Suspense fallback={<LoadingSpinner />}>
18 <DataComponent />
19 </Suspense>
20 </ErrorBoundary>
21 );
22}
23
24// Retry on error
25function RetryableSection({ queryKey }) {
26 return (
27 <ErrorBoundary
28 FallbackComponent={ErrorFallback}
29 onReset={() => {
30 queryClient.invalidateQueries({ queryKey });
31 }}
32 resetKeys={[queryKey]}
33 >
34 <Suspense fallback={<Skeleton />}>
35 <DataComponent queryKey={queryKey} />
36 </Suspense>
37 </ErrorBoundary>
38 );
39}Streaming SSR with Suspense#
1// Next.js App Router
2// app/page.tsx
3import { Suspense } from 'react';
4
5async function SlowComponent() {
6 const data = await fetchSlowData(); // Takes 2 seconds
7 return <div>{data}</div>;
8}
9
10async function FastComponent() {
11 const data = await fetchFastData(); // Takes 100ms
12 return <div>{data}</div>;
13}
14
15export default function Page() {
16 return (
17 <div>
18 {/* Fast content streams immediately */}
19 <Suspense fallback={<FastSkeleton />}>
20 <FastComponent />
21 </Suspense>
22
23 {/* Slow content streams when ready */}
24 <Suspense fallback={<SlowSkeleton />}>
25 <SlowComponent />
26 </Suspense>
27 </div>
28 );
29}
30
31// The HTML streams progressively:
32// 1. Shell with fallbacks sent immediately
33// 2. FastComponent content injected (~100ms)
34// 3. SlowComponent content injected (~2s)Transitions with Suspense#
1import { useTransition, useState, Suspense } from 'react';
2
3function TabContainer() {
4 const [tab, setTab] = useState('home');
5 const [isPending, startTransition] = useTransition();
6
7 function selectTab(nextTab: string) {
8 startTransition(() => {
9 setTab(nextTab);
10 });
11 }
12
13 return (
14 <div>
15 <TabButtons
16 selectedTab={tab}
17 onSelect={selectTab}
18 isPending={isPending}
19 />
20
21 <div className={isPending ? 'pending' : ''}>
22 <Suspense fallback={<TabSkeleton />}>
23 {tab === 'home' && <HomeTab />}
24 {tab === 'posts' && <PostsTab />}
25 {tab === 'contact' && <ContactTab />}
26 </Suspense>
27 </div>
28 </div>
29 );
30}
31
32// Keep showing old content while loading new
33// isPending lets you show loading indicator without hiding contentPreloading Data#
1// Preload on hover/focus
2const preloadUser = (userId: string) => {
3 queryClient.prefetchQuery({
4 queryKey: ['user', userId],
5 queryFn: () => fetchUser(userId),
6 });
7};
8
9function UserLink({ userId, children }) {
10 return (
11 <Link
12 to={`/users/${userId}`}
13 onMouseEnter={() => preloadUser(userId)}
14 onFocus={() => preloadUser(userId)}
15 >
16 {children}
17 </Link>
18 );
19}
20
21// Preload component code
22const UserProfile = lazy(() => import('./UserProfile'));
23
24function preloadUserProfile() {
25 import('./UserProfile');
26}
27
28function UserLink({ userId }) {
29 return (
30 <Link
31 to={`/users/${userId}`}
32 onMouseEnter={() => {
33 preloadUserProfile();
34 preloadUser(userId);
35 }}
36 >
37 View Profile
38 </Link>
39 );
40}Custom Suspense-Enabled Hook#
1// Create a resource for Suspense
2function createResource<T>(promise: Promise<T>) {
3 let status: 'pending' | 'success' | 'error' = 'pending';
4 let result: T;
5 let error: Error;
6
7 const suspender = promise.then(
8 (data) => {
9 status = 'success';
10 result = data;
11 },
12 (err) => {
13 status = 'error';
14 error = err;
15 }
16 );
17
18 return {
19 read(): T {
20 switch (status) {
21 case 'pending':
22 throw suspender;
23 case 'error':
24 throw error;
25 case 'success':
26 return result;
27 }
28 },
29 };
30}
31
32// Usage
33const userResource = createResource(fetchUser(userId));
34
35function UserProfile() {
36 const user = userResource.read(); // Suspends if pending
37 return <div>{user.name}</div>;
38}Best Practices#
Boundaries:
✓ Place boundaries at meaningful UI sections
✓ Use granular boundaries for independent content
✓ Combine with ErrorBoundary
✓ Consider user experience flow
Performance:
✓ Preload on user intent
✓ Use streaming SSR
✓ Avoid loading waterfalls
✓ Show meaningful skeletons
Patterns:
✓ Use transitions for tab switching
✓ Keep old content visible during loads
✓ Parallel data fetching
✓ Progressive enhancement
Conclusion#
React Suspense simplifies loading state management. Place boundaries strategically for optimal UX, combine with error boundaries for resilience, and use transitions to keep the UI responsive. With streaming SSR, you can deliver fast initial loads while progressively enhancing the page.