React

Suspense: what it suspends and what it does not

Suspense shows a fallback while a component waits for a promise: it works with lazy and use, not with a fetch started inside useEffect.

Key points

  • Suspense catches a promise thrown during render: without that promise it shows nothing.
  • A fetch started in useEffect does not suspend: the component renders the loading state you wrote yourself.
  • The fallback replaces the whole subtree, so nested boundaries keep the swap small.

Suspense suspends only a component that throws a promise during render: React shows the fallback and tries again when the promise resolves. A fetch started inside useEffect suspends nothing.

What really suspends

Three cases enter the mechanism: lazy() loading a module, use(promise) from React 19 reading a promise you already created, and data libraries that throw the promise for you. Everything else stays ordinary state.

A runnable case

import { Suspense, lazy } from "react";

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

export function Scheda({ aperta }) {
  if (!aperta) return null;
  return (
    Caricamento}>
      
    
  );
}

The file runs in a Vite project with React 19: Suspense wraps the lazy component and the fallback appears until the module arrives. The rest of the page stays interactive.

Skeleton or text

The fallback is interface: a skeleton that keeps the height avoids the layout jump, a short line of text costs less to read. Either way the content needs stable dimensions, or Cumulative Layout Shift gets worse exactly where you meant to improve it.

Live records in the catalogue

The skeleton category collects the placeholders in the catalogue, with a ready block skeleton; for a short wait there is the Kibo UI spinner, MIT licence.

The limitation

Suspense does not fetch data on its own and does not retry a rejected promise: an error inside the suspended subtree needs an error boundary, or the page stays on the fallback. There is no configurable timeout, so you cannot tell React to wait two hundred milliseconds at most.

Where to continue

Streaming with Suspense is in streaming and Suspense in Next.js; the server component model is in a guide to server components; what changed in the release is in React 19, what changes. The comparison between the two most used frameworks is in Vue 3 vs React.

Frequently asked questions

Does Suspense work with a fetch in useEffect?

No. A fetch started in useEffect throws no promise during render, so Suspense never sees it and shows the loading state you wrote yourself.

Do I need React 19 to use Suspense?

No for lazy, yes for use. Suspense has existed since 16.6 and loads lazy components; reading a promise with use needs React 19.

What is the difference between Suspense and a loading if?

The control sits where the promise is born. An if depends on state you update; Suspense depends on a render that throws the promise.

Can I put Suspense inside a list?

Yes, but one boundary per row produces many fallbacks. Use one boundary around the list and row skeletons inside the fallback.

Does Suspense replace an error boundary?

No. Suspense handles waiting and an error boundary handles failure: you need both, with the boundary above the Suspense.