Suspense for Data Fetching
Overview
Historically, when a component needed data, it rendered a loading spinner, ran a useEffect to fetch the data, and then re-rendered the data. This caused 'Waterfalls': if <Parent> fetched data, <Child> couldn't even begin fetching its data until the Parent finished.
React 18 introduced `<Suspense>` as a first-class citizen for orchestrating asynchronous loading states.
When a Server Component is awaiting a database query, or a Client Component is suspended via a library like React Query, React detects the suspension. It reaches up the component tree until it hits a <Suspense fallback={<Spinner />}> boundary, and immediately displays that fallback UI to the user while the component finishes loading in the background. This completely decouples the loading UI from the data fetching logic, resulting in drastically cleaner code.
Syntax
import { Suspense } from 'react';
import DashboardChart from './DashboardChart';
function DashboardLayout() {
return (
<main>
<h1>Monthly Analytics</h1>
<p>Here is the data for this month:</p>
{/* While the AsyncComponent is awaiting its database query,
React will instantly render this <p> tag in its place! */}
<Suspense fallback={<p className="animate-pulse">Loading chart data...</p>}>
<DashboardChart />
</Suspense>
</main>
);
}
// --- DashboardChart.js (Server Component) ---
export default async function DashboardChart() {
// This pause triggers the Suspense boundary in the parent
const data = await db.query('...');
return <Chart data={data} />;
}Common Pitfalls
- Suspense with standard useEffect: You cannot simply wrap a traditional
useEffect/fetchcomponent in<Suspense>and expect it to work. Traditional fetches do not throw Promises, so React doesn't know they are loading. Suspense only works with Server Components, React Query, SWR, or the experimentaluse()hook.
Interview Questions
Suspense ONLY handles the 'loading' state. To handle errors (e.g., the database connection fails), you must wrap the <Suspense> boundary inside an <ErrorBoundary> component. If the async component throws an error, the ErrorBoundary catches it and displays a fallback crash UI.
Normally, a server waits for ALL database queries to finish before sending the HTML to the browser. With Suspense, the server immediately sends the HTML for the static layout and the Suspense fallbacks (spinners). As each async component finishes its database query, the server 'streams' the final HTML chunk down the open network pipe, and React seamlessly swaps the spinner with the real content. This drastically improves perceived loading speeds.
Real-World Example
Granular vs Page-level Suspense: By using granular Suspense boundaries, you prevent one slow database query (like fetching 10,000 product reviews) from blocking the user from seeing the actual Product Details or adding the item to their cart.
export default function Storefront() {
return (
<div className="grid">
{/* STRATEGY 1: Granular Suspense */}
{/* The rest of the page remains interactive while reviews load */}
<Suspense fallback={<SkeletonReviews />}>
<ProductReviews />
</Suspense>
<Suspense fallback={<SkeletonRecommendations />}>
<RelatedProducts />
</Suspense>
</div>
);
}
// STRATEGY 2: Next.js 'loading.js' file
// In Next.js, creating a 'loading.js' file automatically wraps the entire
// page.js file in a giant Suspense boundary under the hood!Check Your Knowledge
Test your understanding of Suspense for Data Fetching with these quick questions.