Skip to main content
Saved
Pattern
Difficulty Advanced

Async Boundary

Define fallback UI to show while asynchronous components or data are loading.

By Den Odell Added

Async Boundary

Problem

Ever watched a dashboard load? First the header appears, then a spinner where the chart should be, then the sidebar pops in, then another spinner, then the chart finally shows up while a third section is still loading. It’s visual chaos.

Without coordinated loading states, every component is on its own. One widget shows a skeleton, another shows a spinner, a third just sits there empty. Users have no idea what’s loaded and what’s still coming, and the whole page feels janky.

The worst part is a single page-level spinner that blocks everything until even the least important widget loads. Users could be reading main content, but instead they’re staring at a spinner because some corner widget is slow.

Solution

Wrap related components in boundaries that coordinate their loading states. Think of it like a stage curtain: you don’t reveal the set piece by piece, you wait until everything’s ready and show it all at once.

Define boundaries around logical sections: main content, sidebar, widget panel. Everything inside a boundary shows a single fallback until all children are ready, then appears together. No flickering, no piece-by-piece appearance.

The key insight is nesting. Put important content in its own boundary so it appears first; less critical sections get their own boundaries and load independently. Critical content appears fast while secondary content takes its time, but within each section, things still appear together.

Example

The pattern is the same across frameworks: wrap async content, provide a fallback, let the framework coordinate.

Basic Async Boundary

import { Suspense } from 'react';

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

function UserProfile({ userId }) {
  const user = use(fetchUser(userId)); // React 19+ use() hook
  return (
    <div>
      <h1>{user.name}</h1>
      <p>{user.bio}</p>
    </div>
  );
}

Nested Boundaries for Progressive Loading

Separate boundaries for each section let content appear independently rather than waiting for everything:

function Dashboard() {
  return (
    <div className="dashboard">
      <Suspense fallback={<MainContentSkeleton />}>
        <MainContent />
      </Suspense>
      <Suspense fallback={<SidebarSkeleton />}>
        <Sidebar />
      </Suspense>
      <Suspense fallback={<ActivitySkeleton />}>
        <ActivityFeed />
      </Suspense>
    </div>
  );
}

Multiple Async Dependencies

Grouping related components in one boundary makes them appear together:

// All three must load before the boundary resolves
function ProductPage({ productId }) {
  return (
    <Suspense fallback={<PageSkeleton />}>
      <ProductDetails productId={productId} />
      <ProductReviews productId={productId} />
      <RelatedProducts productId={productId} />
    </Suspense>
  );
}

Streaming with Server Components

Server components stream HTML as data becomes available, showing static content immediately:

// Next.js App Router - static content renders immediately, async streams in
export default function Page() {
  return (
    <div>
      <Header />
      <Suspense fallback={<ProductGridSkeleton />}>
        <ProductGrid />
      </Suspense>
      <Footer />
    </div>
  );
}

Delayed Fallback

Delaying the fallback prevents spinner flash for fast loads:

// Avoid spinner flash for fast loads by delaying 200ms
function PageWithTimeout() {
  const [showFallback, setShowFallback] = useState(false);
  useEffect(() => {
    const timer = setTimeout(() => setShowFallback(true), 200);
    return () => clearTimeout(timer);
  }, []);

  return (
    <Suspense fallback={showFallback ? <Spinner /> : null}>
      <SlowComponent />
    </Suspense>
  );
}

Benefits

  • UI loads in coherent chunks rather than flickering piece by piece. Entire sections appear together, which feels polished.
  • Loading states become predictable; users know what’s loading and what’s ready because sections transition as units.
  • Components don’t manage their own loading state. They just render data while boundaries handle coordination.
  • Progressive loading becomes natural: critical content appears first while secondary content loads in its own boundary, and server streaming fills in content as data arrives.

Tradeoffs

  • You need framework support: React Suspense, Vue Suspense, Svelte’s await blocks. Without first-class support, you’re building it yourself.
  • One slow child blocks the whole boundary. Four widgets with one taking five seconds means users wait five seconds for all four.
  • Boundary placement is tricky: too few means coarse loading that blocks too much, too many means spinners everywhere.
  • Debugging nested boundaries gets harder since you need to track which boundary is waiting on what.
  • Not all data fetching works with boundaries. Fetching in useEffect won’t trigger Suspense, so you may need to refactor.
  • No built-in timeout means a child that never resolves keeps the boundary waiting forever unless you add timeout logic.
  • Error handling needs extra work: combine async boundaries with error boundaries, or one failure takes down the whole section.

Summary

Async boundaries separate synchronous rendering from asynchronous operations by defining clear loading and error states at component boundaries. Combined with error boundaries, they let you show loading indicators while data fetches and catch failures gracefully. This declarative approach keeps async complexity out of components and makes loading states predictable.

Newsletter

A Monthly Email
from Den Odell

Behind-the-scenes thinking on frontend patterns, site updates, and more

No spam. Unsubscribe anytime.