Suspense
Suspense lets a component "wait" for something — code, data, an image — and render a fallback while it waits. Originally for lazy-loaded components, it now powers data fetching in React 18+ with frameworks like Next, Remix, and any library that integrates with React.use(). The win: declarative loading boundaries instead of every component handling its own spinner.
Suspense for lazy components, images, and data
EXAMPLE
import React, { Suspense, lazy, use } from 'react';
// 1) Code-split a route. The bundle loads on first render of LazyProfile.
const LazyProfile = lazy(() => import('./Profile'));
function App() {
return (
<Suspense fallback={<PageSkeleton />}>
<LazyProfile userId='u1' />
</Suspense>
);
}
// 2) Suspense for data with the use() hook (React 19+).
// The component reads a promise — Suspense catches it and shows the fallback.
function UserCard({ userId }) {
const user = use(fetchUser(userId)); // throws the promise on first call
return <div>{user.name} — {user.email}</div>;
}
// fetchUser caches by id so re-render does not re-fetch
const userCache = new Map();
function fetchUser(id) {
if (!userCache.has(id)) {
userCache.set(id, fetch(\`/api/users/${id}\`).then(r => r.json()));
}
return userCache.get(id);
}
// 3) Nested boundaries — independent loading regions
function Dashboard() {
return (
<Layout>
<Suspense fallback={<Skeleton lines={4} />}>
<UserCard userId='u1' />
</Suspense>
<Suspense fallback={<Skeleton lines={8} />}>
<ActivityFeed />
</Suspense>
</Layout>
);
}
// 4) Streaming SSR (Next.js / React Server Components):
// The server flushes the shell, then streams each Suspense boundary
// as its data resolves. The browser paints progressively.
// export default async function Page() { return (
// <Suspense fallback={<Spinner/>}>
// <SlowProductGrid />
// </Suspense>
// ); }
// 5) Pair with ErrorBoundary for errors inside the suspended region
// function Safe({ children }) {
// return <ErrorBoundary fallback={<Oops/>}><Suspense fallback={<Spinner/>}>{children}</Suspense></ErrorBoundary>;
// }
// 6) Suspense for images — the new <img> in React 19 also supports it
// <Suspense fallback={<Skeleton/>}><img src='/hero.jpg' /></Suspense>
// Useful when you want the layout-shift-free placeholder to share its
// fallback with surrounding data.
function PageSkeleton() { return <div className='animate-pulse'>Loading...</div>; }
function Skeleton({ lines }) {
return <div>{Array.from({ length: lines }).map((_, i) => <div key={i} className='skeleton-row'/>)}</div>;
}
Why it matters
Place Suspense boundaries at meaningful regions of the page, not around every component. Too many fallbacks make the page feel janky as little spinners flicker in and out; one or two well-placed boundaries give the user a smooth "shell appears, content fills in" experience.
Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.
Discussion
Loading…