Lesson 29 of 31
Suspense for Data Fetching
What Suspense is at a conceptual level, its long-stable original use with React.lazy, and why Suspense-based data fetching needs a Suspense-aware source, not any fetch call.
Every lesson on data fetching so far has used explicit loading state:
check a boolean, render a spinner or the real content. Suspense is a
different mechanism for the same underlying problem — letting part of the
tree say "I'm not ready yet" — that shifts where the loading state is
handled.
The concept: a component can "suspend"
<Suspense fallback={<Spinner />}>
<ProfileDetails />
</Suspense>If ProfileDetails isn't ready to produce its real output yet, it can
suspend — signal that to React — and the nearest Suspense boundary
above it renders fallback in its place. Once ProfileDetails is ready,
React swaps the fallback out for the real content. Unlike the manual
pattern from the fetching-in-effects lesson, ProfileDetails itself doesn't
need its own loading state or a conditional if (loading) return <Spinner /> — the boundary owns the fallback, and the component underneath
just renders its real output once it can.
The original, long-stable use case: code-splitting
Suspense shipped, and has been stable, for years before any data-fetching use — its original job was showing a fallback while the JavaScript for a lazily-loaded component is still downloading:
import { lazy, Suspense } from "react";
const HeavyChart = lazy(() => import("./HeavyChart"));
function Dashboard() {
return (
<Suspense fallback={<p>Loading chart...</p>}>
<HeavyChart />
</Suspense>
);
}React.lazy wraps a dynamic import() so the component's code is only
fetched when it's actually needed, rather than bundled into the initial
page load. While that download is in flight, HeavyChart "suspends," and
Suspense shows the fallback until the code arrives and the component can
render for real. This use is uncontroversial and has been part of stable
React for a long time — it's a genuinely safe pattern to reach for today.
Data fetching with Suspense is a different story
Extending the same idea to data — suspending a component until its data has
loaded, instead of until its code has downloaded — is conceptually
appealing, but it needs the data source itself to know how to "suspend" in
a way React understands (in practice, this means throwing a promise that
React catches and waits on). A plain fetch() call inside a component body
doesn't do that on its own; wrapping a component that just calls fetch in
a <Suspense> boundary does not, by itself, make it work.
In practice, Suspense-based data fetching works when the data comes from
something specifically built to support it: a framework's built-in data
loading layer, or a library designed around Suspense. The use hook,
stabilized in React 19, is the more direct primitive for this — it can read
a promise during render and integrates with Suspense if that promise isn't
settled yet:
import { use, Suspense } from "react";
function ProfileDetails({ userPromise }) {
const user = use(userPromise); // suspends until userPromise resolves
return <p>{user.name}</p>;
}
function Profile({ userPromise }) {
return (
<Suspense fallback={<p>Loading...</p>}>
<ProfileDetails userPromise={userPromise} />
</Suspense>
);
}The important detail is where userPromise comes from: it needs to be a
promise created appropriately (often by a framework's data layer, or a
cache designed for this), not a fresh fetch(...) call created inline on
every render — a fresh promise recreated on every render would restart the
loading state endlessly instead of resolving once and staying resolved.
use also reads context conditionally
A secondary, genuinely useful feature of use: unlike useContext, it can
be called conditionally, since it isn't bound by the rules-of-hooks
restriction the same way (it's technically not a hook in the traditional
sense):
function Banner({ show }) {
if (show) {
const theme = use(ThemeContext); // allowed — use isn't restricted to the top level
return <div className={theme}>Banner</div>;
}
return null;
}The practical takeaway
Understand Suspense conceptually as "a boundary that shows a fallback while
something below it isn't ready," trust it fully for code-splitting with
React.lazy today, and treat Suspense-based data fetching as something
that depends on the specific data layer you're using (a framework's loaders,
a Suspense-aware cache) rather than something any arbitrary fetch call
gets automatically. The manual loading/error/data pattern from
earlier in this course remains the honest, portable default when you're not
working inside a framework that wires this up for you.
Try it yourself
Suspense with React.lazy needs a real dynamic import and multiple
modules, which doesn't fit in a single-file playground — but the shape below
demonstrates the concept safely: a promise-returning "resource" and a
component that reads it, wrapped in a boundary showing a fallback while it
resolves.
// Illustrative shape only — not runnable as a single file, since real
// Suspense-based fetching depends on a data layer built to support it.
const resource = fetchProfileData(); // a Suspense-aware cache would return this
function ProfileDetails() {
const user = resource.read(); // throws the pending promise if not ready
return <p>{user.name}</p>;
}
function App() {
return (
<Suspense fallback={<p>Loading profile...</p>}>
<ProfileDetails />
</Suspense>
);
}What to remember
- Suspense lets part of the tree say "not ready yet," rendering the nearest boundary's fallback in its place — the boundary owns the fallback, not the component.
- Its original, long-stable use is code-splitting with React.lazy — safe and standard to use today.
- Suspense-based data fetching needs a data source specifically built to integrate with it (a framework's loaders, a Suspense-aware cache); a plain fetch call inside a component doesn't suspend on its own.
- The
usehook (React 19) is the modern primitive for reading a promise or context during render, but the promise it reads still typically needs to come from something Suspense-aware, not a fresh call recreated every render.
Check yourself
4 questions · pass 3/4 to unlock Testing React Components
1.What does Suspense fundamentally let a component do?
2.What was Suspense's original, long-stable use case, well before data fetching?
3.Can you make an arbitrary existing fetch() call work with Suspense just by wrapping the component in a <Suspense> boundary?
4.What is the
usehook (stabilized in React 19) used for, in relation to Suspense?
4 left to answer