React Suspense

Coordinate asynchronous loading states declaratively using Suspense boundaries and lazy-loaded component splitting.

TL;DR

  1. Coordinate asynchronous data loading states declaratively using modern React Suspense boundaries.
  2. Split heavy client application bundles on demand using dynamic React.lazy.
  3. Display polished skeleton placeholders using the fallback prop.

Code Splitting with React.lazy

    Dynamic Component Import

    Import component on demand when first rendered.

    const HeavyChart = lazy(
      () => import('./HeavyChart')
    );
    Suspense Boundary Wrapper

    Provide skeleton fallback during chunk download.

    export function Dashboard() {
      return (
        <Suspense fallback={<CardSkeleton />}>
          <HeavyChart />
        </Suspense>
      );
    }
    Named Export Adapter

    Adapt named export to default export for lazy.

    const Analytics = lazy(() =>
      import('./Tools').then((m) => ({
        default: m.Analytics,
      }))
    );

Coordinating Suspense Boundaries

    Nested Granular Boundaries

    Render fast components while slow ones load.

    <Suspense fallback={<HeaderSkeleton />}>
      <Header />
      <Suspense fallback={<FeedSkeleton />}>
        <Feed />
      </Suspense>
    </Suspense>
    Parallel Loading with Boundaries

    Load sibling widgets simultaneously in parallel.

    <RowView>
      <Suspense fallback={<CardSkeleton />}>
        <WidgetA />
      </Suspense>
      <Suspense fallback={<CardSkeleton />}>
        <WidgetB />
      </Suspense>
    </RowView>
    Combined Boundary and Error Boundary

    Handle loading and error states together cleanly.

    <ErrorBoundary FallbackComponent={ErrorCard}>
      <Suspense fallback={<CardSkeleton />}>
        <DataView />
      </Suspense>
    </ErrorBoundary>

Suspense with React 19 Data

    React 19 use Hook

    Read promise directly inside component with use.

    function UserProfile({ promise }: Props) {
      const user = use(promise);
      return <CardTitle>{user.name}</CardTitle>;
    }
    Suspense Preloaded Promise

    Trigger data request before rendering component.

    const userPromise = fetchUser(id);
    return (
      <Suspense fallback={<CardSkeleton />}>
        <UserProfile promise={userPromise} />
      </Suspense>
    );
    Streaming Server Component

    Stream slow content chunks down as ready.

    <Suspense fallback={<ListSkeleton />}>
      <AsyncServerReviews />
    </Suspense>

Suspense Best Practices

    Skeleton UI Alignment

    Match skeleton dimensions to final rendered output.

    // Prevent layout shift with exact skeleton sizes
    <Box style={{ height: 240, width: '100%' }} />
    Avoid Spinner Cascades

    Group related components under a single boundary.

    // Better UX: Single boundary for coherent screen
    <Suspense fallback={<FullPageSkeleton />}>
      <ProfileView />
    </Suspense>
    Route-Level Code Splitting

    Split page bundles cleanly at router boundaries.

    <Route
      path="/analytics"
      element={
        <Suspense fallback={<PageSpinner />}>
          <AnalyticsPage />
        </Suspense>
      }
    />

Tips

  1. Nest multiple <Suspense> boundaries across your component tree to reveal ready content immediately without waiting for slower siblings.
  2. Pair <Suspense> boundaries with <ErrorBoundary> components to handle both loading states and unexpected fetch failures gracefully.

Warnings

  1. Do not wrap every tiny component in its own boundary; excessive independent loading spinners create jarring visual layout shifts.
  2. Ensure all dynamic module imports supplied to React.lazy export their target UI components as the default export.

In Practice

FAQ