Suspense cheatsheet.

What Suspense does

Pauses rendering until something is ready. Shows fallback while paused. Reveals content when ready.

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

If UserPage (or anything inside) “suspends,” the fallback renders.

What suspends

  • React.lazy() imports
  • The use() hook with a pending Promise/Context
  • Server Components fetching data (in RSC frameworks)
  • TanStack Query / Relay / SWR with suspense mode

Code splitting

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

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

Browser loads the chunk on demand.

use(Promise)

function User({ promise }: { promise: Promise<User> }) {
  const user = use(promise);
  return <p>{user.name}</p>;
}

const promise = fetchUser(id);
<Suspense fallback={<Loader />}>
  <User promise={promise} />
</Suspense>

use(promise) suspends until it resolves.

Promise from above

Don’t create promises during render — they get re-created each render.

// BAD: new promise per render
function Wrapper({ id }: { id: number }) {
  const p = fetchUser(id);
  return <User promise={p} />;
}

// GOOD: lift / memoize
const promise = useMemo(() => fetchUser(id), [id]);

In RSC, fetch is done in the server component and passed down.

Nested boundaries

<Suspense fallback={<PageLoader />}>
  <Header />
  <Suspense fallback={<UserSkeleton />}>
    <User />
  </Suspense>
  <Suspense fallback={<FeedSkeleton />}>
    <Feed />
  </Suspense>
</Suspense>

Header shows immediately. User and Feed show their own skeletons until ready.

Error boundaries

Suspense handles “loading.” Errors need ErrorBoundary:

class ErrorBoundary extends React.Component {
  state = { error: null };
  static getDerivedStateFromError(error) { return { error }; }
  render() {
    if (this.state.error) return this.props.fallback(this.state.error);
    return this.props.children;
  }
}

<ErrorBoundary fallback={(e) => <p>error: {e.message}</p>}>
  <Suspense fallback={<Loader />}>
    <User />
  </Suspense>
</ErrorBoundary>

Use react-error-boundary:

npm i react-error-boundary
import { ErrorBoundary } from "react-error-boundary";

<ErrorBoundary FallbackComponent={({ error }) => <p>{error.message}</p>}>
  <Suspense fallback={<L />}>
    <User />
  </Suspense>
</ErrorBoundary>

SuspenseList (experimental)

<SuspenseList revealOrder="forwards">
  <Suspense fallback={<L1 />}><A /></Suspense>
  <Suspense fallback={<L2 />}><B /></Suspense>
  <Suspense fallback={<L3 />}><C /></Suspense>
</SuspenseList>

Reveal order: forwards / backwards / together.

Streaming SSR

Server starts streaming HTML before all data is ready:

<Suspense fallback={<HeaderSkel />}>
  <Header />               // fast
</Suspense>
<Suspense fallback={<FeedSkel />}>
  <Feed />                 // slow
</Suspense>

Server flushes Header HTML immediately, streams Feed when ready. Faster TTFB and FCP.

Next.js, Remix, and Astro all support this.

TanStack Query suspense

const { data } = useSuspenseQuery({
  queryKey: ["user", id],
  queryFn: () => fetchUser(id),
});

Suspends until data is ready. Always returns data (non-nullable).

Transitions + suspense

const [isPending, startTransition] = useTransition();

function nav(href) {
  startTransition(() => {
    setRoute(href);     // new page suspends
  });
}

While transitioning, current UI stays visible (no fallback flash). isPending indicates load.

Fallback design

  • Skeleton screens > spinners (less jarring).
  • Match dimensions of real content to avoid layout shift.
  • One spinner per logical unit, not nested everywhere.

Avoiding cascades

// BAD: nested suspenses cause sequential loads
<Suspense fallback={<L1 />}>
  <Parent>                    // suspends on data
    <Suspense fallback={<L2 />}>
      <Child />               // suspends on data only after parent loads
    </Suspense>
  </Parent>
</Suspense>

// BETTER: parallel
const parent = fetchParent();
const child = fetchChild();

In RSC frameworks, kick off requests in parallel.

Common mistakes

  • Promise re-created per render — infinite suspend.
  • ErrorBoundary outside Suspense — error doesn’t get caught from suspension.
  • Layout shift between fallback and content.
  • Top-level Suspense only — all-or-nothing loading.
  • use(promise) without wrapping Suspense — error.

Read this next

If you want my Suspense + ErrorBoundary patterns, they’re at rajpoot.dev .


Building something AI-, backend-, or data-heavy and want a second pair of eyes? I do consulting and freelance work — see my projects and ways to reach me at rajpoot.dev .