Suspense is React's way of saying "this component isn't ready yet -- show something else while we wait." It replaces the imperative if (isLoading) return <Spinner /> pattern with a declarative boundary that catches pending operations and renders a fallback automatically. What started as a code-splitting tool in React 16 has grown into the foundation for data fetching, streaming server rendering, and coordinated loading states across your entire component tree.
Master react suspense
Take the React Intermediate course with hands-on lessons and challenges.
Each page becomes a separate chunk that downloads on navigation. The Admin route preloads on hover, so if the user moves their cursor to the link and then clicks, the chunk is already cached. This is the highest-impact code splitting pattern because routes are natural loading boundaries.
The use() hook reads a promise during render. When the promise is pending, the component suspends and React shows the nearest Suspense fallback. When it resolves, React re-renders with the data. The key rule: create or cache the promise outside the suspending component to avoid re-creating it on every render attempt.
Each dashboard section loads independently: if the revenue chart query takes 2 seconds but stat cards resolve in 200ms, users see the stats immediately. The outer boundary catches the header, while inner boundaries control each widget. Skeletons that match the actual component layout prevent layout shift when content appears.
With streaming SSR, the server sends the HTML shell (layout, navigation, skeletons) immediately. As each async Server Component resolves, its HTML is streamed to the browser and swapped in for the skeleton — no client-side JavaScript needed for the initial content. The slow reviews query no longer blocks the entire page.
Error Boundary wraps Suspense because a rejected promise needs to be caught somewhere. Without the Error Boundary, a failed fetch crashes the entire app. With it, users see a retry button scoped to just that section. This Suspense + ErrorBoundary pair is the replacement for the old { data, isLoading, error } pattern — each concern is handled by a dedicated component boundary.
In production, most teams use TanStack Query or SWR instead of raw use() because they handle caching, deduplication, and background refetching. useSuspenseQuery removes the isLoading/error boilerplate entirely — the component only runs when data is available. useSuspenseQueries fires multiple requests in parallel under a single Suspense boundary.
Creating the promise inside the suspending component — e.g., calling `fetch()` directly in the component that calls `use()`. Every time React retries rendering after a suspend, it creates a new promise, causing an infinite loop.
Create or cache the promise in a parent component, a route loader, or a data library. Pass it down as a prop or use a library like TanStack Query that manages promise lifecycle for you. The rule: the component that suspends should receive its promise, not create it.
Wrapping every small component in its own Suspense boundary. A page with 10 separate spinners popping in at different times is worse than a single skeleton that resolves once.
Group related content under a single Suspense boundary so they reveal together. Use separate boundaries only for sections that are visually independent and have meaningfully different load times — a sidebar versus the main content area, not a title versus a subtitle.
Using Suspense without an Error Boundary. If the suspended promise rejects (network error, 500 response), there's nothing to catch it and the error propagates up, potentially crashing the entire app.
Always pair Suspense with an Error Boundary. The Error Boundary wraps the Suspense boundary (outside it, not inside) so it catches both render errors and rejected promises. Provide a retry mechanism in the error fallback.
Expecting `useEffect` fetches or regular async/await in client components to trigger Suspense. They don't — Suspense only activates when a component throws a promise during render, which only happens with `use()`, `React.lazy()`, or Suspense-enabled libraries.
For client-side data fetching with Suspense, use `useSuspenseQuery` from TanStack Query, the `use()` hook with a cached promise, or SWR's suspense mode. Plain `useEffect` + `useState` patterns bypass Suspense entirely.
Suspense is React's declarative primitive for handling asynchronous operations. It started with code splitting via `React.lazy()` in React 16, expanded to data fetching with the `use()` hook in React 19, and powers streaming SSR in frameworks like Next.js. The core mechanic is always the same: a component suspends by throwing a promise, the nearest Suspense boundary shows a fallback, and React re-renders when the promise resolves. Pair every Suspense boundary with an Error Boundary, design skeletons that match your real UI, and use boundary placement to control whether content loads independently or reveals together.
Interactive lessons and challenges on Stanza, practice in VS Code, Cursor, or the web.
Interactive lessons and challenges, right in your code editor.
Check the free courses. No credit card.