React Suspense

Load components and data with Suspense boundaries for better UX and streaming.

TL;DR

  1. 01Use Suspense to show loading states while components or data load.
  2. 02Wrap lazy-loaded components in Suspense for fallback UI.
  3. 03Combine with error boundaries for complete async error handling.

Tips

  1. 01Nest Suspense boundaries at different levels to show partial content incrementally while the rest loads.

Warnings

  1. 01Suspense for data fetching is still experimental in React 19 — check version compatibility and use established libraries like React Query for production apps.

Suspense Basics

  • Wrap Suspense around components that may suspend.
    import { Suspense, lazy } from "react";
    
    const HeavyComponent = lazy(() => import("./HeavyComponent"));
    
    export default function App() {
      return (
        <Suspense fallback={<p>Loading...</p>}>
          <HeavyComponent />
        </Suspense>
      );
    }
  • The fallback prop shows while the component is loading.
  • Once loaded, the component replaces the fallback.
  • Suspense can wrap multiple components at once.
    <Suspense fallback={<div>Loading page...</div>}>
      <Header />
      <MainContent />
      <Sidebar />
    </Suspense>
  • Use a skeleton or spinner as the fallback for a better UX.
    <Suspense fallback={<PageSkeleton />}>
      <Dashboard />
    </Suspense>

Code Splitting with Suspense

  • Use lazy to split code and load components on demand.
    const Dashboard = lazy(() => import("./pages/Dashboard"));
    const Settings = lazy(() => import("./pages/Settings"));
    
    export default function App({ page }) {
      return (
        <Suspense fallback={<p>Loading page...</p>}>
          {page === "dashboard" && <Dashboard />}
          {page === "settings" && <Settings />}
        </Suspense>
      );
    }
  • Each lazy component is a separate code chunk.
  • Chunks load only when the component is about to render.
  • Significantly reduces initial bundle size for large apps.
  • Combine with React Router for route-based code splitting.
    const Home = lazy(() => import("./pages/Home"));
    const Profile = lazy(() => import("./pages/Profile"));
    
    <Suspense fallback={<p>Loading...</p>}>
      <Routes>
        <Route path="/" element={<Home />} />
        <Route path="/profile" element={<Profile />} />
      </Routes>
    </Suspense>

Data Fetching with Suspense

  • Use React Query or SWR with Suspense for data fetching.
    function UserProfile({ userId }) {
      const { data: user } = useSuspenseQuery({
        queryKey: ["user", userId],
        queryFn: () => fetchUser(userId)
      });
      
      return <div>{user.name}</div>;
    }
    
    <Suspense fallback={<p>Loading user...</p>}>
      <UserProfile userId={1} />
    </Suspense>
  • Suspense handles loading states automatically.
  • No need for manual loading state management.
  • Error boundaries catch fetch errors thrown outside Suspense.
  • Wrap the data-fetching component, not the calling component.
    // UserProfile suspends internally — wrap it here
    function Page() {
      return (
        <Suspense fallback={<Spinner />}>
          <UserProfile userId={userId} />
        </Suspense>
      );
    }

Nested Suspense Boundaries

  • Use multiple Suspense boundaries for granular control.
    <Suspense fallback={<p>Loading page...</p>}>
      <Header />
      <Suspense fallback={<p>Loading content...</p>}>
        <MainContent />
      </Suspense>
      <Suspense fallback={<p>Loading sidebar...</p>}>
        <Sidebar />
      </Suspense>
    </Suspense>
  • Each boundary can have its own fallback UI.
  • Inner boundaries resolve independently of each other.
  • Outer fallback shows only for outer-level suspensions.
  • Great for showing partial content while loading.
  • Place boundaries close to each suspending component for best UX.
    // Narrow boundary: only hides the part that's loading
    function ProductList() {
      return (
        <ul>
          {productIds.map(id => (
            <Suspense key={id} fallback={<li>Loading...</li>}>
              <ProductItem id={id} />
            </Suspense>
          ))}
        </ul>
      );
    }

Advanced Patterns

  • Combine Suspense with Error Boundaries.
    <ErrorBoundary fallback={<p>Error loading page</p>}>
      <Suspense fallback={<p>Loading...</p>}>
        <PageContent />
      </Suspense>
    </ErrorBoundary>
  • Transition to new content with useTransition.
    function App() {
      const [isPending, startTransition] = useTransition();
      const [page, setPage] = useState("home");
      
      const navigate = (newPage) => {
        startTransition(() => setPage(newPage));
      };
      
      return (
        <Suspense fallback={<p>Loading...</p>}>
          {isPending && <p>Loading new page...</p>}
          <PageContent page={page} />
        </Suspense>
      );
    }
  • Use startTransition to keep the old UI visible while new content loads.
  • Preload lazy components early to avoid loading delays.
    // Preload on hover before user clicks
    const LazyPage = lazy(() => import("./Page"));
    
    function NavLink({ href, children }) {
      return (
        <a href={href} onMouseEnter={() => import("./Page")}>
          {children}
        </a>
      );
    }
  • Use the react-error-boundary package for ready-made Error Boundaries.
    import { ErrorBoundary } from "react-error-boundary";
    
    <ErrorBoundary fallbackRender={({ error }) => <p>Error: {error.message}</p>}>
      <Suspense fallback={<Spinner />}>
        <AsyncComponent />
      </Suspense>
    </ErrorBoundary>

FAQ