When React introduced Suspense, it gave developers a declarative way to handle asynchronous UI states. But its most visible feature — the fallback prop — has also become its most problematic.
The good news? There are patterns to tame it. Let’s break down the challenges and then examine strategies to address them.
🛑 The Problem with fallback
All-or-nothing UI → Entire sections vanish when a single component suspends.
Context loss → Headers, sidebars, or persistent layouts disappear along with the main content.
Over-engineering → Developers wrap everything in nested Suspense to avoid "blank screen" moments.
SSR streaming weirdness → Fallback flashes, inconsistencies, and unpredictable behavior across server/client rendering.
Fixing the fallback
Prop Problem
React Suspense is often introduced with a magical promise:
<Suspense fallback={<Spinner />}>
<MyComponent />
</Suspense>
That’s it — just drop a fallback
and React will handle loading states for you.
But in practice, the fallback
prop is one of the trickiest parts of Suspense. Used carelessly, it leads to:
- Blank screens 🔲
- Layouts disappearing 😬
- Confusing UX ⚠️
This tutorial shows you why fallback
is problematic and how to fix it with practical patterns.
1. Setup: A Simple Suspense App
Let’s create a fake async component to demonstrate:
// Simulate a fetch that takes 2 seconds
function fetchData() {
return new Promise((resolve) => {
setTimeout(() => resolve("Hello from Suspense!"), 2000);
});
}
// Wrap fetch in a resource (Suspense expects thrown promises)
const resource = {
read() {
const result = fetchData();
throw result; // suspends until resolved
},
};
function AsyncMessage() {
const data = resource.read();
return <p>{data}</p>;
}
Now render it inside Suspense:
import { Suspense } from "react";
export default function App() {
return (
<Suspense fallback={<div>Loading...</div>}>
<AsyncMessage />
</Suspense>
);
}
👉 Problem: Everything inside <Suspense>
disappears while loading.
2. Fix #1: Layout Persistence
Bad (layout disappears):
<Suspense fallback={<div>Loading whole app...</div>}>
<Header />
<MainContent />
</Suspense>
Good (keep header/sidebars visible):
<Header />
<Suspense fallback={<div>Loading main content...</div>}>
<MainContent />
</Suspense>
✅ Users always see the header while the main section loads.
3. Fix #2: Nested Boundaries
Instead of one giant Suspense, add smaller boundaries for independence:
<Layout>
<Suspense fallback={<ProfileSkeleton />}>
<Profile />
</Suspense>
<Suspense fallback={<FeedSkeleton />}>
<Feed />
</Suspense>
</Layout>
- If
Profile
suspends, the feed still renders. - If
Feed
suspends, the profile still renders.
✅ Users see partial content faster.
4. Fix #3: Skeletons Instead of Spinners
Spinners cause UI “jumps.” Skeletons keep the structure visible.
function ArticleSkeleton() {
return (
<div>
<div className="skeleton h-6 w-1/3 mb-2"></div>
<div className="skeleton h-4 w-2/3"></div>
</div>
);
}
<Suspense fallback={<ArticleSkeleton />}>
<Article />
</Suspense>
(with Tailwind, you can style .skeleton
as bg-gray-200 animate-pulse
)
✅ Users know what’s coming, reducing frustration.
5. Fix #4: Parallel vs Sequential Boundaries
- Parallel (faster, independent loading):
<Suspense fallback={<SidebarSkeleton />}>
<Sidebar />
</Suspense>
<Suspense fallback={<ContentSkeleton />}>
<Content />
</Suspense>
- Sequential (load sidebar first, then content):
<Suspense fallback={<PageLoader />}>
<Sidebar />
<Suspense fallback={<ContentSkeleton />}>
<Content />
</Suspense>
</Suspense>
✅ Choose parallel when independent, sequential when dependent.
6. Fix #5: Use Data Libraries
Raw Suspense is low-level. Libraries like React Query (TanStack Query) handle caching and retries, making Suspense boundaries behave better.
import { useSuspenseQuery } from "@tanstack/react-query";
function Profile() {
const { data } = useSuspenseQuery({
queryKey: ["profile"],
queryFn: () => fetch("/api/profile").then((res) => res.json()),
});
return <div>{data.name}</div>;
}
<Suspense fallback={<ProfileSkeleton />}>
<Profile />
</Suspense>
✅ Smarter control with less boilerplate.
7. Fix #6: Streaming with React 18 SSR
On the server, Suspense can progressively stream parts of the UI:
<Suspense fallback={<HeaderSkeleton />}>
<Header />
</Suspense>
<Suspense fallback={<MainSkeleton />}>
<MainContent />
</Suspense>
✅ Users see partial content as soon as it’s ready, instead of waiting for the whole page.
🏁 Final Thoughts
The fallback
prop is powerful but blunt. Out of the box, it causes layout flickers and bad UX.
But with the right patterns, you can make Suspense shine:
- Keep layouts persistent
- Nest boundaries for granularity
- Prefer skeletons over spinners
- Control parallel vs sequential loading
- Use React Query or Relay for smarter data fetching
- Stream progressively on the server
Suspense isn’t magic yet — but with these patterns, it can be production-ready.
Top comments (0)