Suspense & Data Loading

Hard

Suspense is a React component that displays a fallback while its children are loading. Originally for code splitting with React.lazy, Suspense now supports data fetching in React 18+ through frameworks like Next.js. It provides a declarative way to handle async loading states without manual isLoading flags.

Interactive Visualization

Key Points

  • Suspense shows a fallback prop while children are suspended
  • Works with React.lazy for code splitting
  • Works with data fetching in frameworks like Next.js via Server Components
  • Eliminates manual loading state management for supported data sources
  • Nested Suspense boundaries control loading granularity
  • SuspenseList (experimental) coordinates reveal order of multiple suspended items

Code Examples

Nested Suspense Boundaries

import { Suspense, lazy } from 'react'

const UserProfile = lazy(() => import('./UserProfile'))
const UserPosts = lazy(() => import('./UserPosts'))
const Sidebar = lazy(() => import('./Sidebar'))

function UserPage() {
  return (
    <div>
      {/* Outer boundary: entire page skeleton */}
      <Suspense fallback={<PageSkeleton />}>
        <UserProfile />

        {/* Inner boundary: posts load independently */}
        <Suspense fallback={<PostsSkeleton />}>
          <UserPosts />
        </Suspense>

        {/* Another independent boundary */}
        <Suspense fallback={<SidebarSkeleton />}>
          <Sidebar />
        </Suspense>
      </Suspense>
    </div>
  )
}

function PageSkeleton() {
  return <div>Loading page...</div>
}

function PostsSkeleton() {
  return <div>Loading posts...</div>
}

function SidebarSkeleton() {
  return <div>Loading sidebar...</div>
}

Each Suspense boundary independently shows its fallback, letting the profile load and display while posts and sidebar are still loading

Suspense with Server Components (Next.js)

import { Suspense } from 'react'

// Server Component that fetches data
async function UserData({ userId }: { userId: string }) {
  const user = await fetch(`/api/users/${userId}`).then(
    (r) => r.json() as Promise<{ name: string; bio: string }>
  )

  return (
    <div>
      <h2>{user.name}</h2>
      <p>{user.bio}</p>
    </div>
  )
}

// Parent wraps the async component in Suspense
function UserPage({ userId }: { userId: string }) {
  return (
    <div>
      <h1>User Profile</h1>
      <Suspense fallback={<div>Loading user data...</div>}>
        <UserData userId={userId} />
      </Suspense>
    </div>
  )
}

In the Server Components model, async components suspend automatically while awaiting data, and Suspense provides the loading UI

Common Mistakes

  • Placing the Suspense boundary too high, showing a single spinner for the entire page
  • Not providing meaningful skeleton fallbacks that match the content layout
  • Expecting Suspense to work with arbitrary promises without framework support

Interview Tips

  • Explain the evolution: Suspense for code splitting first, then for data fetching
  • Discuss how Suspense eliminates waterfall loading with parallel data fetching
  • Know that Suspense for data fetching requires framework integration, not raw fetch

Related Concepts