React Suspense & Transitions
Suspense lets a React component declare "I'm not ready yet", and lets a parent decide what to show meanwhile. Instead of every component juggling isLoading flags, you wrap part of the tree in <Suspense fallback={<Spinner />}>, and React shows the fallback until everything inside is ready: lazily loaded code, data, or streamed server content.
Suspense works hand in hand with error boundaries (what to show when something fails) and transitions (how to update without flashing back to a spinner). Together they give React apps coordinated loading states, streaming server rendering, and responsive navigation, which are the foundations of Server Components and modern frameworks.
TL;DR
<Suspense fallback={…}>shows the fallback while any child suspends, then reveals the content.- Things that suspend:
lazy()components,use(promise), Suspense-enabled data libraries, and async Server Components. - Place boundaries to design the loading sequence: nested boundaries reveal content progressively.
- Error boundaries catch errors (including rejected promises) and show fallback UI.
- Transitions (
useTransition,startTransition) keep already-visible content on screen during updates instead of re-showing fallbacks. - Start requests early and in parallel to avoid fetch waterfalls hidden inside nested components.
Quick Example
The product header appears as soon as the product loads; reviews and comments fill in independently, each with its own placeholder.
Core Concepts
How Suspending Works
While rendering, a component can signal it's waiting on something asynchronous. React then shows the nearest <Suspense> boundary's fallback for that subtree and retries rendering when the awaited thing is ready. You don't throw promises yourself; you use APIs that integrate with Suspense:
lazy(() => import("./X"))for code.use(promise)for data (React 19). The promise should come from a cache, loader, or Server Component, not be created during render.- Suspense-enabled libraries: TanStack Query's
useSuspenseQuery, Relay, Apollo'suseSuspenseQuery, SWR withsuspense: true, and framework loaders. - Async Server Components that
awaitdata.
Designing Boundaries
Boundaries define what appears together:
- One boundary around a whole page: all or nothing, one spinner.
- Nested boundaries: the shell shows first, then sections stream in as they're ready.
- Sibling boundaries: independent sections reveal in any order.
Place boundaries where a loading placeholder makes sense to users (a sidebar, a feed, a chart), not around every tiny component, which causes "popcorn" UI where things pop in one by one.
Error Boundaries
Suspense handles waiting; error boundaries handle failing. An error boundary catches render errors and rejected use() promises in its subtree and renders fallback UI, often with a retry button. They're class components under the hood; most teams use react-error-boundary. Frameworks provide route-level equivalents (error.tsx in the Next.js App Router).
Transitions
Without transitions, updating something that suspends (switching tabs, navigating, changing a filter) replaces visible content with a fallback, a jarring flash. Wrapping the update in a transition tells React to keep showing the old UI until the new one is ready:
Router navigations in modern frameworks are transitions by default. useDeferredValue is the counterpart for values you receive. Transitions also make expensive renders interruptible, which improves responsiveness. See React performance.
Streaming Server Rendering
With renderToPipeableStream or renderToReadableStream (and frameworks built on them, such as Next.js and Remix/React Router), the server sends HTML for everything outside suspended boundaries immediately, then streams each boundary's HTML as its data resolves, and hydrates sections selectively. Users see meaningful content sooner, and slow sections no longer block the whole page.
Avoiding Waterfalls
Suspense makes it easy to accidentally serialize requests: a parent suspends on its data, and only after it renders does the child start fetching its own data.
- Start fetches early: in route loaders, Server Components, or event handlers, before rendering the components that consume them. Pass promises down.
- Fetch in parallel: start independent requests together, then
use()them where needed. - Prefetch on intent: hover or viewport prefetching for likely navigations.
- Colocate data requirements with the router: file-based loaders and Server Components let the framework parallelize.
Best Practices
Match Skeletons to Final Layout
Fallbacks shaped like the content they replace (skeleton rows, image placeholders with the right dimensions) prevent layout shift, which is good for users and for CLS in Core Web Vitals.
Pair Every Important Boundary With an Error Boundary
A data section that can suspend can also fail. Give users a local error message and a retry, instead of letting one failed widget take down the page.
Use Transitions for Navigation and Filters
Anything that swaps visible content for content that needs loading should be a transition. Show pending state subtly (dimming, a progress bar) rather than replacing the UI with a spinner.
Keep Promises Stable
Promises passed to use() must be cached or created outside render. Creating a new promise on every render causes infinite suspension loops.
Common Mistakes
Creating Promises During Render
One Boundary at the Root
A single top-level boundary means any slow request blanks the entire app. Add boundaries around independent regions so the shell and fast content render immediately.
Forgetting Transitions on Updates
Switching tabs without startTransition flashes the fallback every time, even when the old content could have stayed visible for the 200 ms the new content needed.
FAQ
Can I use Suspense for data fetching today?
Yes, through frameworks (Next.js App Router, React Router), Suspense-enabled libraries (TanStack Query's useSuspenseQuery, Relay, Apollo), or use() with promises from a cache or loader. Hand-rolled Suspense data fetching with ad hoc promises is discouraged; use one of these integrations.
What's the difference between Suspense and an error boundary?
Suspense shows a fallback while children are loading. An error boundary shows a fallback when children throw an error. They're complementary, and typically nested together around the same region.
Why does my content flash back to a spinner when I navigate?
The update wasn't a transition, so React showed the nearest fallback. Wrap the state update in startTransition (or use your router's navigation, which does this) so React keeps the old content visible until the new content is ready.
Does Suspense work with server rendering?
Yes. Streaming SSR sends the HTML shell immediately and streams each Suspense boundary's content as its data resolves, with selective hydration on the client. That's the core of the Next.js App Router and React Server Components.
Related Topics
- React — The library overview
- React Server Components — Async components that stream
- React Hooks —
use,useTransition, anduseDeferredValue - Data Fetching — Fetching strategies in frontends
- TanStack Query — Suspense-enabled server state
- React Performance — Transitions and code splitting