Skip to the chapter
←ReactGGroundwork⚛React
React/Advanced/A3
36 / 57 written chapters
/

The cover
  1. B1Setup & JSX
  2. B2Thinking in React
  3. B3Components
  4. B4Props
  5. B5useState
  6. B6Events & conditionals
  7. B7Lists & keys
  8. B8Forms
  9. B9useEffect
  10. B10Lifting state & styling
  11. B11Guided project
  12. ★Cheat page
  1. I1useRef
  2. I2Context
  3. I3useReducer
  4. I4Custom hooks
  5. I5Effects in depth
  6. I6Routing
  7. I7State architecture
  8. I8Memoisation
  9. I9Boundaries & portals
  10. I10Data fetching
  11. I11Forms at scale
  12. I12State libraries
  13. I13Auth
  14. I14TanStack Query
  15. I15Testing
  16. I16TypeScript
  17. I17Animation
  18. I18E2E testing
  19. I19i18n
  20. I20File uploads
  21. I21Real-time UI
  22. I22shadcn/ui
  23. ★Cheat page
  1. A1Fiber
  2. A2Concurrent
  3. A3Suspense
  4. A4Server Components
  5. A5Server Actions
  6. A6use()
  7. A7Rendering strategies
  8. A8Effect timing
  9. A9External stores
  10. A10Refs in depth
  11. A11Component patterns
  12. A12Performance
  13. A13Accessibility
  14. A14Design systems
  15. A15Advanced TypeScript
  16. A16Architecture at scale
  17. A17Custom renderers
  18. A18Migrating & upgrading
  19. A19Production debugging
  20. A20Review & judgement
  21. A21State machines
  22. A22Feature flags
  23. A23Interview bank
  24. ★Cheat page

⚛A3 · Part 3 · Advanced

Suspense & code splitting

Declaring where a loading state lives, instead of threading one through every component.

6 min read9 sections2 exercises
36 of 57

In this chapter

  • The idea
  • Code splitting, the everyday use
  • Boundary placement is a design decision
  • Nested boundaries reveal outside in
  • Suspense and transitions together

and 4 more

about 6 min
09On this page
  1. The idea
  2. Code splitting, the everyday use
  3. Boundary placement is a design decision
  4. Nested boundaries reveal outside in
  5. Suspense and transitions together
  6. Resetting a boundary on navigation
  7. Streaming SSR
  8. What can suspend
  9. Pair it with an error boundary

The idea

<Suspense fallback={<Skeleton />}>
  <Profile />      // if this suspends, the skeleton shows
</Suspense>

A component "suspends" when it needs something that is not ready yet. React stops rendering that subtree, shows the nearest boundary's fallback, and retries when the thing arrives. The component itself contains no isLoading check — the loading state moved out, to a place that can describe the whole region.

The mechanism is a thrown promise, which is why older articles describe it that way. You never throw one yourself: you reach it through lazy, use, or a library that supports Suspense.

Code splitting, the everyday use

const Settings = lazy(() => import("./Settings"));

<Suspense fallback={<Spinner />}>
  <Settings />
</Suspense>

lazy returns a component that suspends until its chunk arrives. The natural split points are routes, heavy dialogs, and anything below the fold that most visitors never open — an editor, a chart library, a map. The module must have a default export; for a named one, map it: lazy(() => import("./Chart").then((m) => ({ default: m.Chart }))).

Prefetch on intent, not on render: onMouseEnter={() => import("./Settings")} starts the download while the pointer is travelling, so the click feels instant.

The most common mistake
function Page() {
  const Chart = lazy(() => import("./Chart"));   // ✗ inside the component
}

A new lazy component on every render means a new type, so React unmounts and remounts it every time — the chunk reloads and the fallback flashes forever. lazy belongs at module scope.

Boundary placement is a design decision

// one boundary: nothing appears until everything is ready
<Suspense fallback={<PageSkeleton />}>
  <Header /><Feed /><Sidebar />
</Suspense>

// three: each region appears as it becomes ready
<Suspense fallback={<HeaderSkeleton />}><Header /></Suspense>
<Suspense fallback={<FeedSkeleton />}><Feed /></Suspense>

Finer boundaries are not automatically better. Too many and the page assembles in visible stages, shifting layout as each lands. Aim for regions a user would describe as separate things, and give the fallback the same shape as the content so nothing moves when it swaps.

Page Header (ready) <Suspense fallback={<FeedSkeleton />}> Feed (still loading) FeedSkeleton (shown now)
The boundary owns the loading state for its region. Header is outside it, so it appears at once.

Nested boundaries reveal outside in

<Suspense fallback={<PageSkeleton />}>
  <Article />
  <Suspense fallback={<CommentsSkeleton />}>
    <Comments />
  </Suspense>
</Suspense>

While Article is loading, the page skeleton shows. When it is ready the article appears, and if comments are still loading their own skeleton takes their place. Nesting is how you say "this part may arrive later, but that part must not". Whatever is inside the outer boundary and outside the inner one appears together, as a unit.

React also paces the reveals: suspended content is revealed at most once every 300ms, and boundaries that become ready inside that window appear together instead of popping in one by one. Since React 19, when something suspends, the fallback is committed straight away and the suspended siblings are rendered afterwards to start their requests early, so the fallback appears faster than it did in React 18.

Suspense and transitions together

startTransition(() => setTab("analytics"));

Without the transition, switching to a tab whose data is not ready replaces the current content with a fallback — the screen goes blank and comes back. Inside a transition, React keeps the old content on screen until the new one is ready, and isPending lets you dim it in the meantime.

This is the rule precisely: once a boundary is showing content, suspending again brings the fallback back unless the update came from startTransition or useDeferredValue. It is the difference between a page that flashes on every navigation and one that feels continuous, and it is why routers wrap navigation in a transition. When content is hidden again, React cleans up its layout effects and runs them again once it reappears.

Resetting a boundary on navigation

<ProfilePage key={userId} />

Going from one profile to another inside a transition keeps the old profile visible while the new one loads, which is usually wrong: the user asked for a different person. A key tells React this is different content, so the boundary resets and shows its fallback. The key can sit on the boundary or any component above it.

Streaming SSR

On the server, Suspense boundaries are the unit of streaming. The server sends the shell immediately with fallbacks in place, then streams each boundary's HTML as its data resolves. The user sees the page structure straight away instead of waiting for the slowest query.

<Layout>
  <Suspense fallback={<FeedSkeleton />}>
    <Feed />        // slow query — streams in later, does not block the shell
  </Suspense>
</Layout>

Selective hydration follows the same boundaries: React hydrates the parts the user interacts with first, so a click on a ready section works even while another is still arriving.

Boundaries also contain server errors. If a component throws while rendering on the server, React does not abort the page. It puts the nearest boundary's fallback into the HTML and tries that component again on the client. One failing widget costs you a spinner, not a 500 — and if the client render fails too, the error boundary takes over.

What can suspend

SuspendsDoes not
lazy() componentsA bare fetch in an effect or event handler
Framework data APIs, and Server ComponentsuseState loading flags
Query libraries in suspense modeAnything you have not opted in
use(promise) — see the use hook

Suspense does not make ordinary fetching declarative on its own. The data source has to participate, which is why "just use Suspense for loading states" does not work in a plain Vite app without a library.

One more caveat: state is not kept for a render that suspended before it ever mounted. When the data arrives React renders that tree from scratch, so a component that suspends on first render cannot rely on state it set before suspending.

Pair it with an error boundary

<ErrorBoundary fallback={<Failed />}>
  <Suspense fallback={<Skeleton />}>
    <Profile />
  </Suspense>
</ErrorBoundary>

Suspense handles "not yet"; an error boundary handles "never". A chunk that fails to download — a deploy mid-session, a flaky connection — throws, and without a boundary that is a blank page.

Recovering is not the same as catching. A lazy component remembers that its import failed, so re-rendering it will not try again. After a deploy the old chunk name may simply no longer exist. The dependable recovery for a failed chunk is a fallback with a button that reloads the page; for failed data, the boundary's reset should also trigger a fresh request, otherwise it retries into the same rejected promise.

Interview answer, one sentence

"Suspense moves the loading state out of the component and into a boundary that owns a region, which is what makes streaming SSR and selective hydration possible. Its most important interaction is with transitions: inside one, React keeps the current content visible instead of replacing it with the fallback — and a key is how you opt back out when the content is genuinely different."

02Practice this chapter

Opens in the editor: write it, run it, and check it against real tests.

  • 4 testsThe nearest boundary shows the fallback, and only that oneadvanced
  • 5 testsTwo boundaries, resolving on their own schedulesadvanced
All 54 for this level →
A3

Finished reading?

0 of 57 chapters read in this series

→Next stop

  1. A2Previous · A2Concurrent features
  2. A3you are here
  3. Next · A4Server ComponentsComponents that run once, on the server, and ship no JavaScript at all.7 min read

01On this page

9 sections · 0%

  1. 01The idea
  2. 02Code splitting, the everyday use
  3. 03Boundary placement is a design decision
  4. 04Nested boundaries reveal outside in
  5. 05Suspense and transitions together
  6. 06Resetting a boundary on navigation
  7. 07Streaming SSR
  8. 08What can suspend
  9. 09Pair it with an error boundary

[] chapters · t top