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