Skip to main content

Suspense and Data Fetching

1. What Suspense does while data loads

Suspense lets React pause rendering part of the tree until a component receives the data it needs, and show fallback UI (for example, a spinner or a skeleton).

That is, React can now "wait" not only for lazy() components, but also for a Promise from a data request.


2. What changed in React 18

Before React 18:

  • Suspense could only wait for lazily loaded components (React.lazy);
  • for data you had to manually show Loading, check isLoading, handle errors, and so on.

With React 18+:

  • Suspense can now wait for a Promise from any source (fetch, async functions, and so on);
  • React understands that a component is "not ready yet" and pauses its render, showing the fallback;
  • once the Promise resolves -> React resumes the render with the real data.

3. An example of a "manual" implementation with a Promise

To understand the mechanics, let's look at a "low-level" example without Next.js:

javascript
// utils/fetchData.js export function fetchData(url) { let status = 'pending'; let result; const suspender = fetch(url) .then(res => res.json()) .then(data => { status = 'success'; result = data; }) .catch(err => { status = 'error'; result = err; }); return { read() { if (status === 'pending') throw suspender; if (status === 'error') throw result; return result; }, }; }

Now let's use it in a component:

javascript
const resource = fetchData('/api/user'); function Profile() { const user = resource.read(); // throws a Promise if the data is not ready yet return <h1>{user.name}</h1>; } function App() { return ( <Suspense fallback={<div>Loading profile...</div>}> <Profile /> </Suspense> ); }

What happens:

  1. resource.read() throws a Promise -> React "freezes" the render of the Profile component;
  2. React shows the fallback from Suspense;
  3. When the Promise resolves -> React "resumes" the render and inserts Profile with the data.

4. Data Fetching in React Server Components (Next.js 13+)

In React 18+ and Next.js 13/14, Suspense is deeply integrated into server rendering.

Example:

javascript
// app/page.js (Next.js 13+) import { Suspense } from 'react'; import UserInfo from './UserInfo'; import UserPosts from './UserPosts'; export default async function Page() { const user = await getUser(); // an async server component return ( <> <h1>{user.name}</h1> <Suspense fallback={<div>Loading posts...</div>}> {/* UserPosts loads independently */} <UserPosts userId={user.id} /> </Suspense> </> ); }

Here React:

  • sends the first part of the HTML (the user's name) right away;
  • "pauses" the rendering of UserPosts until it gets the data;
  • later streams in the HTML for UserPosts (Streaming SSR);
  • all of this happens without reloading the page.

This works thanks to new APIs:

  • renderToPipeableStream() - Node.js streaming SSR;
  • renderToReadableStream() - for Deno, Edge, and so on.

5. Combining Suspense with React Query / SWR

React does not yet have a "built-in" fetch mechanism, but popular libraries have already added Suspense support:

javascript
import { useQuery } from '@tanstack/react-query'; import { Suspense } from 'react'; function Todos() { const { data: todos } = useQuery({ queryKey: ['todos'], queryFn: () => fetch('/api/todos').then(res => res.json()), suspense: true, // the key option }); return <ul>{todos.map(t => <li key={t.id}>{t.title}</li>)}</ul>; } export default function App() { return ( <Suspense fallback={<div>Loading tasks...</div>}> <Todos /> </Suspense> ); }

useQuery({ suspense: true }) tells React that it needs to throw a Promise while loading, and Suspense automatically displays the fallback.


6. How Suspense works internally

  1. A component calls async code (or a hook) that throws a Promise;
  2. React "encounters" that Promise;
  3. React:
  • defers the render of that branch;
  • shows the fallback from the nearest <Suspense>;
  1. When the Promise resolves, React resumes the render;
  2. With SSR, React can even stream in the finished HTML to the client.

7. What Suspense does not do on its own

  • It does not perform fetch() directly - your code or a library does that.
  • It does not handle errors (catch) - for that you need an <ErrorBoundary>.
  • It does not replace useEffect - Suspense works during rendering, not after.

Summary

Suspense in React 18 lets React wait for data the same way it waits for components. It turns asynchrony into a declarative construct: React "pauses" part of the UI and "resumes" it once everything is ready, without manual isLoading, if (!data), and so on.


In short:

CapabilityBefore React 18React 18+
Waiting for async componentsNoYes
SSR + Streaming SuspenseNoYes
Integration with React Query / SWRNoYes
Data Fetching in Server ComponentsNoYes
Fallback UI without manual isLoadingNoYes

Short Answer

Interview ready
Premium

A concise answer to help you respond confidently on this topic during an interview.