React

What is React Suspense?

The short answer

React Suspense lets you declaratively specify a loading state for a part of your component tree. You wrap components in a <Suspense> boundary with a fallback prop, and if any child component isn't ready to render yet (it's waiting on code to load, data to fetch, etc.), React shows the fallback until it is. Instead of every component managing its own loading state, Suspense lets you handle it at the tree level.

The basic API

The syntax is straightforward:

import { Suspense } from 'react';

function App() {
return (
<Suspense fallback={<Spinner />}>
<Dashboard />
</Suspense>
);
}

If Dashboard (or anything inside it) isn't ready to render, React shows <Spinner /> in its place. When the suspended component is finally ready, React swaps in the real content. You don't write if (loading) return <Spinner /> inside Dashboard — the boundary handles it.

How it works under the hood

Here's where things get interesting. When a component "suspends," it literally throws a promise. React catches that thrown promise, recognizes it as a signal that the component isn't ready, and walks up the tree to find the nearest <Suspense> boundary. It then renders the fallback.

When the promise resolves, React tries rendering the suspended component again. If it succeeds this time, React replaces the fallback with the actual content.

This mechanism is why you don't use Suspense by throwing promises manually in your components. Instead, you use libraries and APIs that integrate with this protocol — React.lazy for code splitting, and data-fetching libraries like React Query, Relay, and Next.js that know how to "suspend" while data loads.

Suspense for code splitting

The first and most widely used application of Suspense is code splitting with React.lazy. This lets you load components on demand instead of bundling everything upfront:

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

function App() {
return (
<div>
<Navigation />
<Suspense fallback={<p>Loading settings...</p>}>
<Settings />
</Suspense>
</div>
);
}

When the user navigates to settings, React starts loading the Settings chunk. While it loads, the fallback appears. Once the chunk is downloaded and parsed, Settings renders. The rest of the app stays interactive throughout — Navigation never unmounts or shows a spinner.

This is a massive win for initial page load performance. Instead of shipping your entire app in one giant bundle, you split it at route boundaries (or any logical boundary) and load pieces as needed.

Suspense for data fetching

Suspense for data fetching takes the same idea and applies it to async data. Instead of the traditional pattern where every component manages its own loading state:

// The traditional approach — every component handles loading
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);

useEffect(() => {
fetch(`/api/users/${userId}`)
.then((res) => res.json())
.then((data) => {
setUser(data);
setLoading(false);
});
}, [userId]);

if (loading) return <Spinner />;
return <h2>{user.name}</h2>;
}

With Suspense-enabled data fetching, the component just reads the data as if it's already there:

// With Suspense — the component reads data, the boundary handles loading
function UserProfile({ userId }) {
const user = useSuspenseQuery({
queryKey: ['user', userId],
queryFn: () =>
fetch(`/api/users/${userId}`).then((res) =>
res.json()
),
});

return <h2>{user.data.name}</h2>;
}

function App() {
return (
<Suspense fallback={<Spinner />}>
<UserProfile userId="123" />
</Suspense>
);
}

The component doesn't think about loading states at all. If the data isn't ready, the library suspends, and the boundary shows the fallback. The component code stays clean and focused on the happy path.

Nested Suspense boundaries

You can place Suspense boundaries at any level, and they nest naturally. This lets different sections of your page load independently:

function Dashboard() {
return (
<div>
<Suspense fallback={<HeaderSkeleton />}>
<DashboardHeader />
</Suspense>

<div className="dashboard-grid">
<Suspense fallback={<ChartSkeleton />}>
<RevenueChart />
</Suspense>

<Suspense fallback={<TableSkeleton />}>
<RecentOrders />
</Suspense>
</div>
</div>
);
}

The header, chart, and orders table each have their own boundary. If the chart data takes 3 seconds but the orders load in 500ms, the orders appear immediately while the chart still shows its skeleton. Each section resolves independently.

Without nested boundaries, a single top-level Suspense would show one big spinner until everything is ready. Nesting gives you granular control over the loading experience.

Suspense and concurrent features

Suspense becomes even more powerful when combined with concurrent rendering features like useTransition:

function SearchResults() {
const [query, setQuery] = useState('');
const [deferredQuery, setDeferredQuery] = useState('');
const [isPending, startTransition] = useTransition();

const handleChange = (e) => {
setQuery(e.target.value);
startTransition(() => {
setDeferredQuery(e.target.value);
});
};

return (
<div>
<input value={query} onChange={handleChange} />
<Suspense fallback={<ResultsSkeleton />}>
<Results query={deferredQuery} />
</Suspense>
{isPending && (
<p className="loading-hint">Updating...</p>
)}
</div>
);
}

Without startTransition, typing a new search query would immediately trigger the Suspense fallback, replacing the current results with a skeleton. That's jarring — the user loses the content they were looking at.

With startTransition, React keeps showing the current results while preparing the new ones in the background. The isPending flag lets you show a subtle loading indicator without replacing the existing content. This is the "keep the old UI visible while the new one loads" pattern that makes apps feel responsive.

What Suspense doesn't do

Suspense is not a data-fetching library. It doesn't fetch data for you, doesn't cache results, and doesn't handle retries or pagination. It's a rendering mechanism — it tells React what to show while something async is in progress.

You still need a library (React Query, Relay, SWR, Next.js, or a framework-level solution) to actually fetch and manage the data. Suspense just gives those libraries a clean way to integrate with React's rendering cycle.

Why interviewers ask this

Suspense touches on several things interviewers care about: understanding React's rendering model, knowing how to optimize loading experiences, and being familiar with the concurrent features that define modern React. It also reveals whether you've kept up with React's evolution — Suspense has grown from a code-splitting tool to a core part of how React handles async rendering. The best answers show that you understand both the practical usage and the underlying mechanism.