DEV Community

Cover image for Fixing the fallback Prop Problem in React Suspense
Agbo, Daniel Onuoha
Agbo, Daniel Onuoha

Posted on • Edited on

Fixing the fallback Prop Problem in React Suspense

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>
Enter fullscreen mode Exit fullscreen mode

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>;
}
Enter fullscreen mode Exit fullscreen mode

Now render it inside Suspense:

import { Suspense } from "react";

export default function App() {
  return (
    <Suspense fallback={<div>Loading...</div>}>
      <AsyncMessage />
    </Suspense>
  );
}
Enter fullscreen mode Exit fullscreen mode

👉 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>
Enter fullscreen mode Exit fullscreen mode

Good (keep header/sidebars visible):

<Header />
<Suspense fallback={<div>Loading main content...</div>}>
  <MainContent />
</Suspense>
Enter fullscreen mode Exit fullscreen mode

✅ 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>
Enter fullscreen mode Exit fullscreen mode
  • 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>
Enter fullscreen mode Exit fullscreen mode

(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>
Enter fullscreen mode Exit fullscreen mode
  • Sequential (load sidebar first, then content):
<Suspense fallback={<PageLoader />}>
  <Sidebar />
  <Suspense fallback={<ContentSkeleton />}>
    <Content />
  </Suspense>
</Suspense>
Enter fullscreen mode Exit fullscreen mode

✅ 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>
Enter fullscreen mode Exit fullscreen mode

✅ 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>
Enter fullscreen mode Exit fullscreen mode

✅ 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)