Technology · React

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.

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>

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

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

React State ManagementReact Testing Best Practices