AniUI Academy

Suspense for Data Fetching

What Suspense is at a conceptual level, its long-stable original use with React.lazy, and why Suspense-based data fetching needs a Suspense-aware source, not any fetch call.

9 min read

Every lesson on data fetching so far has used explicit loading state: check a boolean, render a spinner or the real content. Suspense is a different mechanism for the same underlying problem — letting part of the tree say "I'm not ready yet" — that shifts where the loading state is handled.

The concept: a component can "suspend"

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

If ProfileDetails isn't ready to produce its real output yet, it can suspend — signal that to React — and the nearest Suspense boundary above it renders fallback in its place. Once ProfileDetails is ready, React swaps the fallback out for the real content. Unlike the manual pattern from the fetching-in-effects lesson, ProfileDetails itself doesn't need its own loading state or a conditional if (loading) return <Spinner /> — the boundary owns the fallback, and the component underneath just renders its real output once it can.

The original, long-stable use case: code-splitting

Suspense shipped, and has been stable, for years before any data-fetching use — its original job was showing a fallback while the JavaScript for a lazily-loaded component is still downloading:

import { lazy, Suspense } from "react";
 
const HeavyChart = lazy(() => import("./HeavyChart"));
 
function Dashboard() {
  return (
    <Suspense fallback={<p>Loading chart...</p>}>
      <HeavyChart />
    </Suspense>
  );
}

React.lazy wraps a dynamic import() so the component's code is only fetched when it's actually needed, rather than bundled into the initial page load. While that download is in flight, HeavyChart "suspends," and Suspense shows the fallback until the code arrives and the component can render for real. This use is uncontroversial and has been part of stable React for a long time — it's a genuinely safe pattern to reach for today.

Data fetching with Suspense is a different story

Extending the same idea to data — suspending a component until its data has loaded, instead of until its code has downloaded — is conceptually appealing, but it needs the data source itself to know how to "suspend" in a way React understands (in practice, this means throwing a promise that React catches and waits on). A plain fetch() call inside a component body doesn't do that on its own; wrapping a component that just calls fetch in a <Suspense> boundary does not, by itself, make it work.

In practice, Suspense-based data fetching works when the data comes from something specifically built to support it: a framework's built-in data loading layer, or a library designed around Suspense. The use hook, stabilized in React 19, is the more direct primitive for this — it can read a promise during render and integrates with Suspense if that promise isn't settled yet:

import { use, Suspense } from "react";
 
function ProfileDetails({ userPromise }) {
  const user = use(userPromise); // suspends until userPromise resolves
  return <p>{user.name}</p>;
}
 
function Profile({ userPromise }) {
  return (
    <Suspense fallback={<p>Loading...</p>}>
      <ProfileDetails userPromise={userPromise} />
    </Suspense>
  );
}

The important detail is where userPromise comes from: it needs to be a promise created appropriately (often by a framework's data layer, or a cache designed for this), not a fresh fetch(...) call created inline on every render — a fresh promise recreated on every render would restart the loading state endlessly instead of resolving once and staying resolved.

use also reads context conditionally

A secondary, genuinely useful feature of use: unlike useContext, it can be called conditionally, since it isn't bound by the rules-of-hooks restriction the same way (it's technically not a hook in the traditional sense):

function Banner({ show }) {
  if (show) {
    const theme = use(ThemeContext); // allowed — use isn't restricted to the top level
    return <div className={theme}>Banner</div>;
  }
  return null;
}

The practical takeaway

Understand Suspense conceptually as "a boundary that shows a fallback while something below it isn't ready," trust it fully for code-splitting with React.lazy today, and treat Suspense-based data fetching as something that depends on the specific data layer you're using (a framework's loaders, a Suspense-aware cache) rather than something any arbitrary fetch call gets automatically. The manual loading/error/data pattern from earlier in this course remains the honest, portable default when you're not working inside a framework that wires this up for you.

Try it yourself

Suspense with React.lazy needs a real dynamic import and multiple modules, which doesn't fit in a single-file playground — but the shape below demonstrates the concept safely: a promise-returning "resource" and a component that reads it, wrapped in a boundary showing a fallback while it resolves.

// Illustrative shape only — not runnable as a single file, since real
// Suspense-based fetching depends on a data layer built to support it.
const resource = fetchProfileData(); // a Suspense-aware cache would return this
 
function ProfileDetails() {
  const user = resource.read(); // throws the pending promise if not ready
  return <p>{user.name}</p>;
}
 
function App() {
  return (
    <Suspense fallback={<p>Loading profile...</p>}>
      <ProfileDetails />
    </Suspense>
  );
}

What to remember

  • Suspense lets part of the tree say "not ready yet," rendering the nearest boundary's fallback in its place — the boundary owns the fallback, not the component.
  • Its original, long-stable use is code-splitting with React.lazy — safe and standard to use today.
  • Suspense-based data fetching needs a data source specifically built to integrate with it (a framework's loaders, a Suspense-aware cache); a plain fetch call inside a component doesn't suspend on its own.
  • The use hook (React 19) is the modern primitive for reading a promise or context during render, but the promise it reads still typically needs to come from something Suspense-aware, not a fresh call recreated every render.

Check yourself

4 questions · pass 3/4 to unlock Testing React Components

up to 50
  1. 1.What does Suspense fundamentally let a component do?

  2. 2.What was Suspense's original, long-stable use case, well before data fetching?

  3. 3.Can you make an arbitrary existing fetch() call work with Suspense just by wrapping the component in a <Suspense> boundary?

  4. 4.What is the use hook (stabilized in React 19) used for, in relation to Suspense?

4 left to answer