Skip to content

Suspense & lazy

Managing loading states by hand means threading isLoading booleans through your components and rendering spinners in a dozen places. Suspense replaces that with a boundary: you wrap part of the tree, give it a fallback, and while anything inside is “not ready,” React shows the fallback instead.

import { Suspense } from "react";
function Page() {
return (
<Suspense fallback={<Spinner />}>
<Profile /> {/* if this suspends, the Spinner shows */}
<Timeline /> {/* until BOTH are ready, then both appear together */}
</Suspense>
);
}

You don’t write “if loading show spinner” anywhere. The boundary owns the loading UI for its subtree, declaratively.

A component suspends when it can’t finish rendering yet because it’s waiting on something — code that hasn’t downloaded (React.lazy), or data (via the use() hook or a Suspense-aware data library). When a component suspends, React “unwinds” to the nearest <Suspense> above it and shows that boundary’s fallback. When the awaited thing resolves, React retries the render and swaps the real content in.

flowchart TB
  boundary["Suspense fallback=Spinner"] --> child["Child suspends
(waiting on code or data)"]
  child --> show["React shows Spinner"]
  show --> resolve["awaited thing resolves"]
  resolve --> retry["React retries render
→ shows real content"]
A suspending child shows the nearest boundary's fallback

The most common use of Suspense is lazy-loading a component so its code isn’t in the initial bundle. React.lazy takes a dynamic import() and returns a component that suspends until the chunk downloads.

import { lazy, Suspense } from "react";
// SettingsPanel's code is a separate chunk, fetched only when first rendered.
const SettingsPanel = lazy(() => import("./SettingsPanel"));
function App({ showSettings }) {
return (
<Suspense fallback={<Spinner />}>
{showSettings && <SettingsPanel />}
</Suspense>
);
}

Until SettingsPanel is actually rendered, its JavaScript is never downloaded — a direct win for initial load time. When it first renders, it suspends (downloading), the fallback shows, then the panel appears.

Suspense boundaries nest, and you place them to control the loading granularity. A boundary high in the tree means “show one big spinner for everything below.” Boundaries deeper down mean “reveal the shell immediately, and show small spinners only for the parts still loading.”

<Suspense fallback={<PageSkeleton />}>
<Header />
<Suspense fallback={<FeedSkeleton />}>
<Feed /> {/* the header shows while only the feed loads */}
</Suspense>
</Suspense>

Choosing where boundaries go is a UX decision: fewer boundaries = simpler but more “all or nothing”; more boundaries = progressive reveal.

What does a `<Suspense fallback={...}>` boundary do?
What does it mean for a component to "suspend"?
What does `React.lazy(() => import("./X"))` achieve?
Why nest multiple Suspense boundaries?