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

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:

Designing Boundaries

Boundaries define what appears together:

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.

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

References