---
title: "React: Suspense and Transitions"
slug: "react-suspense"
category: "frontend"
tags: ["frontend", "react", "suspense", "transitions", "streaming"]
status: "stable"
last_updated: 2026-05-14
summary: "React 19 Suspense boundaries, fallback design, nested boundaries, useTransition vs useDeferredValue, and SSR streaming."
related: ["[[frontend/react]]", "[[frontend/react-server-components]]", "[[frontend/react-hooks]]", "[[frontend/react-performance]]", "[[frontend/nextjs]]", "[[coding/typescript]]"]
---

## Overview

Suspense is the React 19 primitive for declarative loading states. A `<Suspense>` boundary catches any descendant that throws a promise (a server component awaiting data, a client component calling `use()`) and renders the `fallback` until the promise resolves. Transitions are the partner primitive for stale-while-fresh UI: a navigation or filter update that should not flash a spinner. This page covers boundary placement, fallback design, and the SSR streaming model that ties both together.

## Place boundaries where the user expects a wait

A Suspense boundary is a UI commitment: this region may show a fallback. Place a boundary at every point in the tree where waiting is acceptable, and only at those points.

```tsx
<Page>
  <Header />                              {/* loads with the shell */}
  <Suspense fallback={<TableSkeleton />}>
    <ProductTable />                      {/* streams in */}
  </Suspense>
  <Suspense fallback={<SidebarSkeleton />}>
    <Recommendations />                   {/* streams independently */}
  </Suspense>
</Page>
```

One boundary per independent slow region. Wrapping the whole page in one boundary forces the user to wait for the slowest fetch before they see anything.

## Design fallbacks that match the final layout

A good fallback occupies the same box as the resolved content. Same width, same height, same vertical rhythm. Reserve the space; the page must not jump when the data arrives.

- Use a skeleton shape, not a spinner, for content boxes; a spinner reads as "broken" past 300ms.
- Set explicit dimensions on images and media in the fallback so [[seo/core-web-vitals]] CLS stays at zero.
- For lists, render four-to-six skeleton rows; the real list will be longer, but the user sees structure immediately.
- Keep the fallback text-free unless the wait is genuinely long (over two seconds). Random labels read as noise.

## Nest boundaries to control reveal order

Boundaries compose. A nested boundary hides only its own subtree; the outer fallback covers the rest. Use nesting to stream the page top-down.

```tsx
<Suspense fallback={<PageSkeleton />}>
  <ArticleHeader />
  <Suspense fallback={<BodySkeleton />}>
    <ArticleBody />
    <Suspense fallback={<CommentsSkeleton />}>
      <Comments />
    </Suspense>
  </Suspense>
</Suspense>
```

The header renders first, then the body, then the comments. Each region streams independently, and a slow comments fetch never blocks the article. See [[frontend/react-server-components]] for the server-side data-fetching pattern this enables.

## Use `useTransition` for user-initiated updates

A transition tells React: this state update is non-urgent; keep the old UI interactive until the new one is ready. Wrap a setter call in `startTransition`, and the component does not flash a fallback during the update.

```tsx
const [isPending, startTransition] = useTransition();
function selectTab(id: string) {
  startTransition(() => setActiveTab(id));
}
return <Tabs activeTab={activeTab} pending={isPending} onSelect={selectTab} />;
```

The `isPending` flag drives subtle UI: a fade on the old content, a disabled state on the trigger. Without a transition, the suspended descendant unmounts and the user sees a fallback.

## Use `useDeferredValue` for derived expensive UI

Reach for `useDeferredValue` when an input drives an expensive render that should not block typing. The hook returns a value that lags behind the input by one render; React paints the input first, then catches up.

```tsx
const [query, setQuery] = useState("");
const deferred = useDeferredValue(query);
return (
  <>
    <input value={query} onChange={(e) => setQuery(e.target.value)} />
    <ExpensiveResults query={deferred} />
  </>
);
```

The difference from `useTransition`: a transition wraps the setter at the call site; a deferred value wraps the consumed value. Use `useTransition` when you control the state update. Use `useDeferredValue` when the value comes from elsewhere (props, context) and you cannot wrap the setter.

## Stream HTML with SSR, hydrate at the boundary

In Next.js and any framework on the new pipeline, the server streams HTML in chunks. The shell renders immediately; each Suspense boundary streams its content when ready. [[glossary/hydration|Hydration]] is selective: React attaches event handlers to the shell as soon as it arrives, and to each region as that region streams in.

This makes the placement decision matter. A boundary close to the root delays interactivity for everything inside it. A boundary close to the leaf delays only the slow region. Default to leaf-ward boundaries; move them up only when the parent depends on the child's data shape.

## Pair Suspense with error boundaries

Suspense handles the loading state. It does not handle errors. A fetch that rejects throws past the Suspense boundary to the nearest error boundary. Pair every Suspense region with an error boundary so a failed fetch shows a recovery UI, not a blank screen. See [[frontend/react-error-boundaries]] for the patterns, and [[frontend/nextjs]] for the `error.tsx` and `loading.tsx` convention that wires both in by file.

## Related

- [[frontend/react]]
- [[frontend/react-server-components]]
- [[frontend/react-hooks]]
- [[frontend/react-performance]]
- [[frontend/react-error-boundaries]]
- [[frontend/nextjs]]
- [[coding/typescript]]
