skip to content

Suspense Boundaries

Suspense is a declarative loading boundary: a subtree can announce it is not ready and React shows a fallback instead of a half-built UI. Interviewers ask about it because it is the one primitive shared by lazy loading, data fetching, and streaming SSR.

part ofReactoverview, primer and where to startread it →
on this pageshow

explore

questions

13

In React, what does the fallback prop of <Suspense> render, and what decides when the user sees the fallback instead of the boundary's children?

level: juniorimportance: must knowfreq 70%

answer

  1. shown in place of the children
  2. any React node, even null
  3. all children wait together
  4. one commit, no half-drawn tree
  5. no boundary, no default spinner

basics

~20 s

The fallback prop holds the UI React shows in place of a <Suspense> boundary's children while something inside those children is not ready yet. React swaps the real children back in as soon as nothing inside that boundary is suspended.

solid answer

~50 s

`<Suspense>` takes a `fallback` prop and a subtree of children. While anything inside that subtree is suspended — a `React.lazy` component whose code has not arrived, or a component reading data that is not available yet — React renders `fallback` instead of the children. The swap is all-or-nothing for that boundary: if three siblings sit under one boundary and only one of them is suspended, the user sees the fallback and none of the three. Once nothing inside the boundary is suspended any more, React commits the real children and removes the fallback in one step, so you never see a half-drawn subtree. `fallback` accepts any React node — an element, a string, even `null` — and it is a real render, so keep it cheap and stateless. There is no implicit root fallback: a component that suspends with no boundary above it is an error.

go deeper

for a junior

Know that <Suspense> takes a fallback prop, that the fallback replaces everything inside the boundary while any of it is loading, and that the real children come back once nothing inside is still waiting.

for a middle

Be ready to explain the all-or-nothing rule with a concrete example and to say what happens when no boundary exists above a suspending component. Mention that the fallback is a real render whose state is discarded on the swap.

for a senior

Show that you treat the fallback as a design decision: how much screen it covers, how cheap it is to render, and whether an empty fallback is better than a spinner for a peripheral widget. Explain why loading UI belongs to the layout owner rather than the leaf component.

for a principal

Own the convention: where the backstop boundary lives so no suspension can ever reach the root unhandled, what the house skeleton vocabulary is, and how teams avoid every component reinventing its own loading UI.

## What a boundary is `<Suspense>` is an ordinary React component with one special prop, `fallback`. Everything you nest inside it is the real UI. `fallback` is the placeholder React puts on screen in that subtree's place while something inside it is not ready. ```jsx <Suspense fallback={<p>Loading chart…</p>}> <RevenueChart /> </Suspense> ``` Nothing about `RevenueChart` changes. The boundary is a declaration about the *hole in the screen*: "if anything under here isn't ready, show this instead." ## What "not ready" means A component becomes suspended when it tries to render something that has not arrived yet. In React 19 the two everyday sources are a component created with `React.lazy` whose module has not loaded, and a component that reads data that is still pending — a Server Component awaiting its data, or a client component reading a resource that has not settled. From the boundary's point of view the cause does not matter: any suspension inside its subtree puts it into fallback state. ## All-or-nothing within one boundary This is the part candidates get wrong. A boundary does not render the children that are ready and a fallback for the one that is not. It renders `fallback` *instead of the entire subtree*. ```jsx <Suspense fallback={<Skeleton />}> <Header /> {/* ready */} <Feed /> {/* suspended */} <Footer /> {/* ready */} </Suspense> ``` The user sees `<Skeleton />` and none of the three. `Header` and `Footer` appear only when `Feed` is ready too. This is deliberate: React reveals a consistent screen rather than a progressively assembling one, and it is exactly why boundary placement matters — the boundary defines how much of the page waits on the slowest thing inside it. A boundary reacts only to suspensions *inside itself*. A sibling boundary elsewhere in the tree going into fallback has no effect on it. ## The fallback is a normal render `fallback` accepts any React node. `fallback={<Spinner />}`, `fallback="Loading…"` and `fallback={null}` are all legal; `null` means "render nothing here while we wait", which is occasionally what you want for a small, non-blocking widget. Because it is a normal render, the fallback's components genuinely mount when the boundary suspends and unmount when the content is revealed. Two consequences follow. First, any state inside the fallback is thrown away on the swap — never store anything meaningful there. Second, an expensive fallback costs real render time at exactly the moment the app is already waiting, so a fallback should be cheap: a skeleton, a spinner, a placeholder box. ## The swap back When the last suspended thing inside the boundary resolves, React renders the children and removes the fallback in the same commit. There is no intermediate frame where half the children are on screen; the transition from placeholder to content happens in one paint. ## There is no default fallback A suspension travels up the tree until it finds a boundary. If there is none, React has nothing to render in its place, so it raises an error telling you to add a `<Suspense fallback=…>` higher in the tree. React never invents a spinner for you and never silently blanks the app. In practice this means every code path that can suspend must have a boundary somewhere above it — which is why frameworks and shells usually keep at least one boundary near the root as a backstop, even when the interesting boundaries are much deeper. ## What this buys you The payoff is that loading state stops being a per-component `if (loading) return <Spinner/>` branch scattered through your code. The component that needs data just reads it, and an ancestor — often owned by whoever owns the layout — decides what the waiting UI looks like and how much of the screen it covers. Loading UI becomes a property of the layout instead of a property of every leaf component.

  • Can the fallback prop be null, and when would you want that?
    Yes — `fallback={null}` is legal and renders nothing while the subtree is suspended. It is reasonable for a small, non-essential widget where a spinner would be more distracting than an empty space, and for a boundary whose only job is to stop a suspension from reaching an ancestor whose fallback would blank out far more of the screen.
  • Two sibling components under the same boundary each load data. One resolves in 50 ms and the other in two seconds. When does the fast one appear?
    After two seconds, with the slow one. A boundary is all-or-nothing: while anything inside it is suspended, the whole subtree is replaced by the fallback. If you want the fast component on screen at 50 ms, it needs to be outside that boundary or inside its own boundary.
  • Does a component that suspends lose the state it had before?
    Not if it had already rendered. When an already-revealed boundary suspends again, React hides its children rather than unmounting them, so their state survives and comes back when the content is revealed. State is only lost when the components genuinely unmount, for example when the fallback's own components are swapped out.

saying these in an interview costs you the question

  • Thinks ready siblings render while only the pending one is replaced
  • Believes React shows a built-in spinner when no boundary exists
  • Puts state or data fetching inside the fallback element
  • Thinks fallback must be a single spinner component, not any node
  • Assumes a suspension is caught by every boundary in the tree

context

open as a page

A React 19 Client Component contains `const user = use(fetch('/api/user').then(r => r.json()))` directly in its body. The UI never leaves the loading fallback and the network panel fills with repeated requests to /api/user. What is happening, and what fixes it?

level: middleimportance: must knowfreq 52%

basics

~20 s

Each render calls fetch again, so use() receives a brand-new pending promise every time. React retries the render when a promise settles, the retry starts another request, and it suspends again — an endless fetch-suspend-retry loop.

open as a page

When a React component suspends, which <Suspense> boundary shows its fallback, and how does nesting boundaries change how much of the screen is replaced?

level: middleimportance: must knowfreq 62%

basics

~20 s

The nearest <Suspense> boundary above the suspended component shows its fallback; boundaries further up are unaffected. Nesting boundaries therefore shrinks the blast radius, letting an inner section swap to a small placeholder while everything outside it stays on screen.

open as a page

During React 19 streaming server rendering, what is the "shell" of the response, and by what mechanism does the HTML for a slow subtree inside a <Suspense> boundary reach the browser after the shell has already been sent?

level: middleimportance: must knowfreq 58%

basics

~20 s

The shell is everything outside any still-pending Suspense boundary; React flushes it first, with each pending boundary represented by its fallback markup. When a slow subtree resolves, React streams its real HTML inside a hidden container plus a tiny inline script that moves those nodes into the boundary's position and removes the fallback.

open as a page

A React list that is already on screen flips back to its <Suspense> fallback every time the user changes a filter that triggers a new data read. Why does the revealed content disappear, and how do you keep it visible while the new data loads?

level: seniorimportance: must knowfreq 58%

basics

~20 s

The filter change is an urgent update, so re-suspending the boundary immediately swaps the revealed list for its fallback. Marking that update as a transition with startTransition or useTransition tells React to keep the old content on screen until the new data is ready.

open as a page

In React server rendering, what does it mean to "stream" the HTML rather than render the whole page before sending anything, and what does streaming actually improve for the user?

level: juniorimportance: should knowfreq 50%

basics

~20 s

Streaming server rendering sends HTML in pieces as it becomes ready instead of buffering the whole page. React flushes the parts that are done immediately and delivers slow, Suspense-wrapped sections later, so the first bytes and first paint arrive much sooner.

open as a page

In React 19, what actually happens inside React when a component suspends while rendering, and what does that imply about the work the component had already done in that render?

level: middleimportance: should knowfreq 44%

basics

~20 s

Suspending unwinds that component's render: React abandons the in-progress work, shows the nearest Suspense fallback, subscribes to the pending promise, and runs the component again from the top when it settles. Nothing it rendered is committed.

open as a page

Suspense data reading is usually described as requiring a cache keyed by request identity. Beyond memoizing a resolved value, what must such a cache guarantee for use() to behave correctly in React 19?

level: seniorimportance: should knowfreq 36%

basics

~20 s

It must return the identical promise for a given key on every render, dedupe requests already in flight, remember rejections as well as results, and be scoped per request on the server so data cannot leak between users.

open as a page

A React page uses streaming server rendering and Suspense boundaries deep in the tree, yet time-to-first-byte is still ~900 ms because a top-level component above every boundary awaits a slow API before rendering. Explain why the deeper boundaries do not help, and what actually fixes it.

level: seniorimportance: should knowfreq 45%

basics

~20 s

Nothing can be flushed until the shell is renderable, and the shell is everything outside pending Suspense boundaries — including that top-level await. Boundaries below it are irrelevant. The fix is to move the slow work inside a boundary, or start the request early and pass the unawaited promise down to a suspended child.

open as a page

On a streamed React 19 page, three sibling <Suspense> boundaries finish loading in a different order than they appear in the document. In what order does their content appear to the user, and how would you make a group of boundaries reveal top-to-bottom instead?

level: seniorimportance: should knowfreq 38%

basics

~20 s

Sibling boundaries reveal in completion order, not document order, so a later section can pop in first. Stable React 19 has no reveal-order coordinator, so you enforce order structurally: nest boundaries so an outer one must reveal before its children, or group the sections behind a single boundary that reveals them together.

open as a page

React 19 ships use() and <Suspense> but no client-side data cache of its own. Which responsibilities does React deliberately leave to a framework or data library, and how would you decide who owns them in an app you are leading?

level: principalimportance: should knowfreq 26%

basics

~20 s

React defines only the render-time protocol: suspend on a pending promise, retry when it settles, surface rejection to an error boundary. Caching, deduping, invalidation, revalidation, retries and server-to-client data transfer are policy, and a framework or library supplies them.

open as a page

How do you decide where to place <Suspense> boundaries across a React page, and what goes wrong at each extreme — one boundary around everything versus a boundary around every component?

level: principalimportance: should knowfreq 40%

basics

~20 s

Place a boundary where a placeholder can replace a region without moving anything around it, and where that region's data genuinely arrives at a different time from its neighbours. One boundary makes the whole page wait on its slowest part; a boundary per component produces a flickering mosaic that reflows repeatedly.

open as a page

A React Server Components render produces a serialized RSC payload in addition to HTML, and that payload is streamed too. How does a <Suspense> boundary shape it, and why does that matter for a client-side navigation where no new HTML document is fetched?

level: seniorimportance: nice to knowfreq 30%

basics

~20 s

The RSC payload is a stream of serialized rows describing the rendered server tree. A suspended subtree is emitted as a placeholder reference immediately, with its resolved rows arriving later in the same stream. On a client navigation the browser fetches that payload instead of HTML, so boundaries still let part of the new screen render before the slow parts arrive.

open as a page