Skip to main content

React Suspense Deep Dive

Answer

Suspense is a React component that lets you "wait" for some code to load or data to fetch, showing a fallback UI while waiting.

How Suspense Works

Basic Usage

import { Suspense } from "react";

function App() {
return (
<Suspense fallback={<Loading />}>
<AsyncContent />
</Suspense>
);
}

// For code splitting
const LazyComponent = React.lazy(() => import("./HeavyComponent"));

function App() {
return (
<Suspense fallback={<Loading />}>
<LazyComponent />
</Suspense>
);
}

Data Fetching with Suspense

// Using a Suspense-compatible library (React Query, SWR, Relay)
function UserProfile({ userId }) {
// This hook "suspends" while loading
const { data: user } = useSuspenseQuery(["user", userId], fetchUser);

// Only renders when data is ready
return <h1>{user.name}</h1>;
}

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

Nested Suspense Boundaries

function App() {
return (
<Suspense fallback={<PageSkeleton />}>
<Header /> {/* Immediate */}
<Suspense fallback={<SidebarSkeleton />}>
<Sidebar /> {/* Independent loading */}
</Suspense>
<Main>
<Suspense fallback={<ContentSkeleton />}>
<Content /> {/* Independent loading */}
</Suspense>
</Main>
</Suspense>
);
}

Streaming SSR

// Server renders components progressively
async function Page() {
return (
<html>
<body>
<Header /> {/* Sent immediately */}
<Suspense fallback={<LoadingPosts />}>
<Posts /> {/* Streamed when ready */}
</Suspense>
<Suspense fallback={<LoadingComments />}>
<Comments /> {/* Streamed when ready */}
</Suspense>
</body>
</html>
);
}

SuspenseList (Experimental)

import { SuspenseList, Suspense } from "react";

function Feed() {
return (
<SuspenseList revealOrder="forwards" tail="collapsed">
<Suspense fallback={<PostSkeleton />}>
<Post id={1} />
</Suspense>
<Suspense fallback={<PostSkeleton />}>
<Post id={2} />
</Suspense>
<Suspense fallback={<PostSkeleton />}>
<Post id={3} />
</Suspense>
</SuspenseList>
);
}

// revealOrder options:
// "forwards" - reveal top to bottom
// "backwards" - reveal bottom to top
// "together" - reveal all at once when all ready

Error Boundaries with Suspense

import { ErrorBoundary } from "react-error-boundary";

function App() {
return (
<ErrorBoundary fallback={<ErrorMessage />}>
<Suspense fallback={<Loading />}>
<AsyncContent />
</Suspense>
</ErrorBoundary>
);
}

// Combined handling
function DataSection() {
return (
<ErrorBoundary
fallbackRender={({ error, resetErrorBoundary }) => (
<div>
<p>Error: {error.message}</p>
<button onClick={resetErrorBoundary}>Retry</button>
</div>
)}
>
<Suspense fallback={<Skeleton />}>
<DataComponent />
</Suspense>
</ErrorBoundary>
);
}

Transitions with Suspense

import { useTransition, Suspense } from "react";

function TabContainer() {
const [tab, setTab] = useState("home");
const [isPending, startTransition] = useTransition();

function handleTabChange(nextTab) {
startTransition(() => {
setTab(nextTab);
});
}

return (
<>
<TabButtons
currentTab={tab}
onChange={handleTabChange}
isPending={isPending}
/>

<Suspense fallback={<TabSkeleton />}>
{tab === "home" && <HomeTab />}
{tab === "posts" && <PostsTab />}
{tab === "profile" && <ProfileTab />}
</Suspense>
</>
);
}
// With useTransition, old tab stays visible while new tab loads

Creating Suspense-Compatible Resources

// Simplified resource pattern
function createResource(promise) {
let status = "pending";
let result;

const suspender = promise.then(
(data) => {
status = "success";
result = data;
},
(error) => {
status = "error";
result = error;
}
);

return {
read() {
if (status === "pending") throw suspender;
if (status === "error") throw result;
return result;
},
};
}

// Usage
const userResource = createResource(fetchUser());

function UserProfile() {
const user = userResource.read(); // Suspends or returns data
return <h1>{user.name}</h1>;
}

Best Practices

// ✅ Place Suspense boundaries at meaningful UI chunks
// ✅ Use multiple boundaries for independent content
// ✅ Combine with Error Boundaries
// ✅ Use with useTransition for tab/route changes

// ❌ Don't wrap everything in one Suspense boundary
// ❌ Don't create resources in render (use hooks/libraries)
// ❌ Don't overuse - adds complexity

Key Points

  • Suspense handles loading states declaratively
  • Uses Promise-throwing mechanism internally
  • Nest boundaries for independent loading
  • Enables streaming SSR with progressive hydration
  • Combine with Error Boundaries for error handling
  • Use useTransition for smoother tab/navigation
  • Works with React.lazy, data fetching libraries