DEV Community

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

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