Search across all documentation pages
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥
Enable the suspense option on useSWR to integrate with React Suspense boundaries. When suspense is active, SWR throws a promise during loading, letting <Suspense> handle the fallback UI.
"use client";
import { Suspense } from "react";
import useSWR from "swr";
const fetcher = (url: string) => fetch(url).then((r) => r.json());
function UserName({ id }: { id: string }) {
// With suspense: true, data is guaranteed to be defined
const { data } = useSWR(`/api/users/${id}`, fetcher, { suspense: true });
return <span>{data.name}</span>;
}
export default function Page() {
return (
<Suspense fallback={<div>Loading user...</div>}>
<UserName id="1" />
</Suspense>
);
}"use client";
import { Suspense } from "react";
import useSWR, { SWRConfig } from "swr";
interface Post {
id: string;
title: string;
body: string;
}
interface Comment {
id: string;
text: string;
author: string;
}
const fetcher = (url: string) => fetch(url).then((r) => r.json());
function PostContent({ postId }: { postId: string }) {
const { data: post } = useSWR<Post>(`/api/posts/${postId}`, fetcher, {
suspense: true,
});
return (
<article>
<h1>{post!.title}</h1>
<p>{post!.body}</p>
</article>
);
}
function PostComments({ postId }: { postId: string }) {
const { data: comments } = useSWR<Comment[]>(
`/api/posts/${postId}/comments`,
fetcher,
{ suspense: true }
);
return (
<ul>
{comments!.map((c) => (
<li key={c.id}>
<strong>{c.author}</strong>: {c.text}
</li>
))}
</ul>
);
}
export default function PostPage({ postId }: { postId: string }) {
return (
<SWRConfig value={{ suspense: true }}>
<Suspense fallback={<div>Loading post...</div>}>
<PostContent postId={postId} />
</Suspense>
<Suspense fallback={<div>Loading comments...</div>}>
<PostComments postId={postId} />
</Suspense>
</SWRConfig>
);
}suspense: true, SWR throws a promise when data is not yet available. React's <Suspense> boundary catches this and renders the fallback.data is guaranteed to be defined (not undefined).useSWR calls inside the same <Suspense> boundary will trigger parallel fetches, but the boundary waits for all of them.<Suspense> boundaries around each data-dependent component enable independent loading states and streaming.Global suspense via SWRConfig:
<SWRConfig value={{ suspense: true }}>
<Suspense fallback={<Loading />}>
{children}
</Suspense>
</SWRConfig>Nested suspense for progressive loading:
<Suspense fallback={<HeaderSkeleton />}>
<Header />
<Suspense fallback={<ContentSkeleton />}>
<MainContent />
<Suspense fallback={<SidebarSkeleton />}>
<Sidebar />
</Suspense>
</Suspense>
</Suspense>Combined with ErrorBoundary:
import { ErrorBoundary } from "react-error-boundary";
<ErrorBoundary fallback={<div>Something went wrong</div>}>
<Suspense fallback={<div>Loading...</div>}>
<DataComponent />
</Suspense>
</ErrorBoundary>suspense: true, data is still typed as Data | undefined by default. Use a non-null assertion or cast when you know suspense guarantees data presence.function useSuspenseSWR<T>(key: string, fetcher: (url: string) => Promise<T>) {
const result = useSWR<T>(key, fetcher, { suspense: true });
return { ...result, data: result.data as T };
}
// data is typed as User, not User | undefined
const { data } = useSuspenseSWR<User>("/api/me", fetcher);useTransition with SWR may have edge cases.fallback in SWRConfig to avoid hydration mismatches.isLoading and isValidating have different semantics in suspense mode. The component simply does not render while loading, so you never see isLoading: true inside the component.suspense: true without a <Suspense> boundary, React will throw an error up to the nearest Error Boundary or crash the app.null keys) disable suspense behavior for that hook. The component will render with data: undefined.| Approach | Pros | Cons |
|---|---|---|
| SWR suspense mode | Clean loading states, composable | SSR complexity, type narrowing needed |
| Manual isLoading checks | Full control, explicit | Repetitive loading UI code |
| React Server Components | No client loading state needed | Cannot use hooks, no real-time updates |
| use() hook (React 19) | Native promise unwrapping | Experimental, different API |
With suspense: true, SWR throws a promise when data is not yet available. React's <Suspense> boundary catches this promise, renders the fallback, and re-renders the component once the promise resolves.
Yes, at render time data is guaranteed to be available because the component only renders after the promise resolves. However, TypeScript still types it as Data | undefined by default. Use a non-null assertion or a wrapper hook to narrow the type.
React will throw the promise upward. If there is no <Suspense> boundary, it reaches the nearest Error Boundary or crashes the app entirely.
<SWRConfig value={{ suspense: true }}>
<Suspense fallback={<Loading />}>
{children}
</Suspense>
</SWRConfig>Yes. Multiple useSWR calls within the same <Suspense> boundary trigger parallel fetches. The boundary waits for all of them before rendering.
No. Only the initial load suspends. Background revalidations update data silently without triggering the Suspense fallback again.
Errors in suspense mode are thrown during render, so they are caught by the nearest Error Boundary. Wrap your <Suspense> inside an <ErrorBoundary>:
<ErrorBoundary fallback={<div>Error</div>}>
<Suspense fallback={<div>Loading...</div>}>
<DataComponent />
</Suspense>
</ErrorBoundary>SWR falls back to client-side fetching during SSR. You need to prepopulate the cache with fallback in SWRConfig to avoid hydration mismatches between server and client.
function useSuspenseSWR<T>(key: string, fetcher: (url: string) => Promise<T>) {
const result = useSWR<T>(key, fetcher, { suspense: true });
return { ...result, data: result.data as T };
}
// data is typed as T, not T | undefinedConditional keys (null) disable suspense behavior for that hook. The component renders immediately with data: undefined instead of suspending.
Wrap each data-dependent section in its own <Suspense> boundary. This enables independent loading states and streaming, so faster sections render without waiting for slower ones.
Reviewed by Chris St. John·Last updated Jul 7, 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥