Suspense Patterns
React Suspense patterns for declarative loading states, data fetching, and code splitting
You are an expert in React Suspense patterns for building React applications.
## Key Points
- **Suspense Boundary**: A `<Suspense fallback={...}>` wrapper that catches thrown promises from child components and shows a fallback until they resolve.
- **Lazy Loading**: `React.lazy()` dynamically imports a component, suspending until the module is loaded.
- **Suspense-enabled Data Fetching**: Libraries like React Query, Relay, and Next.js integrate with Suspense so that data-fetching components suspend automatically.
- **Transitions**: `useTransition` and `startTransition` mark state updates as non-urgent, keeping the current UI visible while new content suspends in the background.
- **Nested Boundaries**: Multiple Suspense boundaries let you control loading granularity — show a skeleton for a section while the rest of the page is interactive.
- Place Suspense boundaries around independently loading sections rather than wrapping the entire app in one boundary.
- Use `startTransition` for tab switches and navigations to keep the current UI responsive.
- Prefetch data and code on hover or during idle time to reduce perceived latency.
- Pair Suspense with Error Boundaries — a suspended component that rejects needs an error boundary to catch the failure.
- Provide meaningful skeleton UIs in fallbacks that match the layout of the loaded content.
- **Waterfall fetching**: Nesting suspense-enabled components causes sequential data fetches. Prefetch in parallel at the route level.
- **Wrapping everything in one boundary**: A single top-level Suspense hides all content until the slowest resource resolves.skilldb get react-patterns-skills/Suspense PatternsFull skill: 174 linesSuspense Patterns — React Patterns
You are an expert in React Suspense patterns for building React applications.
Overview
React Suspense lets components declaratively "wait" for something before rendering — whether that is lazily loaded code, fetched data, or any asynchronous resource. A <Suspense> boundary catches the pending state and renders a fallback while the resource resolves. Combined with transitions and streaming server rendering, Suspense enables fine-grained, non-blocking loading experiences.
Core Philosophy
Suspense exists because loading states are a UI concern, not a data-fetching concern. Before Suspense, every component that fetched data had to manage its own loading, error, and data state, then render conditional branches for each. This scattered loading logic across dozens of components, made it impossible to coordinate loading states across siblings, and forced developers to choose between showing a spinner per component or a single spinner for the whole page.
Suspense inverts the responsibility. The component that needs data simply reads it — if the data is not ready, the component suspends, and a boundary higher in the tree decides what to show while waiting. This declarative model means loading UIs are defined at the layout level, not the data level. You can wrap an entire page section in one boundary for a unified skeleton, or wrap individual widgets for granular loading — and you can change that decision without touching the data-fetching components at all.
The deeper principle is progressive disclosure. Not every part of a page loads at the same speed, and users should not wait for the slowest section before seeing anything. Nested Suspense boundaries combined with streaming server rendering let the fast parts of the page appear immediately while slow parts fill in as they resolve. Combined with transitions that keep stale UI visible during navigation, Suspense eliminates the jarring flash-of-empty-screen pattern that plagued earlier React applications.
Core Concepts
- Suspense Boundary: A
<Suspense fallback={...}>wrapper that catches thrown promises from child components and shows a fallback until they resolve. - Lazy Loading:
React.lazy()dynamically imports a component, suspending until the module is loaded. - Suspense-enabled Data Fetching: Libraries like React Query, Relay, and Next.js integrate with Suspense so that data-fetching components suspend automatically.
- Transitions:
useTransitionandstartTransitionmark state updates as non-urgent, keeping the current UI visible while new content suspends in the background. - Nested Boundaries: Multiple Suspense boundaries let you control loading granularity — show a skeleton for a section while the rest of the page is interactive.
Implementation Patterns
Code Splitting with React.lazy
import { lazy, Suspense } from "react";
const Dashboard = lazy(() => import("./Dashboard"));
const Settings = lazy(() => import("./Settings"));
function App() {
return (
<Suspense fallback={<PageSkeleton />}>
<Routes>
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/settings" element={<Settings />} />
</Routes>
</Suspense>
);
}
Nested Suspense Boundaries
function ProfilePage({ userId }: { userId: string }) {
return (
<Suspense fallback={<PageSkeleton />}>
<ProfileHeader userId={userId} />
<Suspense fallback={<PostsSkeleton />}>
<ProfilePosts userId={userId} />
</Suspense>
<Suspense fallback={<FriendsSkeleton />}>
<FriendsList userId={userId} />
</Suspense>
</Suspense>
);
}
Transitions to Avoid Loading Flickers
import { useState, useTransition } from "react";
function TabContainer() {
const [tab, setTab] = useState("home");
const [isPending, startTransition] = useTransition();
function selectTab(nextTab: string) {
startTransition(() => {
setTab(nextTab);
});
}
return (
<div>
<nav>
<button onClick={() => selectTab("home")}>Home</button>
<button onClick={() => selectTab("posts")}>Posts</button>
<button onClick={() => selectTab("contact")}>Contact</button>
</nav>
<div style={{ opacity: isPending ? 0.7 : 1 }}>
<Suspense fallback={<TabSkeleton />}>
{tab === "home" && <Home />}
{tab === "posts" && <Posts />}
{tab === "contact" && <Contact />}
</Suspense>
</div>
</div>
);
}
Suspense-Ready Data Hook (React Query example)
import { useSuspenseQuery } from "@tanstack/react-query";
function UserProfile({ userId }: { userId: string }) {
const { data: user } = useSuspenseQuery({
queryKey: ["user", userId],
queryFn: () => fetch(`/api/users/${userId}`).then((r) => r.json()),
});
return (
<div>
<h1>{user.name}</h1>
<p>{user.email}</p>
</div>
);
}
// Wrap in Suspense boundary
<Suspense fallback={<ProfileSkeleton />}>
<UserProfile userId="123" />
</Suspense>
Prefetching to Eliminate Waterfalls
import { queryClient } from "./queryClient";
function prefetchUser(userId: string) {
return queryClient.prefetchQuery({
queryKey: ["user", userId],
queryFn: () => fetch(`/api/users/${userId}`).then((r) => r.json()),
});
}
// Prefetch on hover or route transition
<Link to={`/users/${id}`} onMouseEnter={() => prefetchUser(id)}>
View Profile
</Link>
Best Practices
- Place Suspense boundaries around independently loading sections rather than wrapping the entire app in one boundary.
- Use
startTransitionfor tab switches and navigations to keep the current UI responsive. - Prefetch data and code on hover or during idle time to reduce perceived latency.
- Pair Suspense with Error Boundaries — a suspended component that rejects needs an error boundary to catch the failure.
- Provide meaningful skeleton UIs in fallbacks that match the layout of the loaded content.
Common Pitfalls
- Waterfall fetching: Nesting suspense-enabled components causes sequential data fetches. Prefetch in parallel at the route level.
- Wrapping everything in one boundary: A single top-level Suspense hides all content until the slowest resource resolves.
- Missing Error Boundaries: Without an error boundary, a failed async resource crashes the whole tree.
- Suspending during urgent updates: Wrapping a text input's state update in
startTransitionmakes typing feel laggy. Only mark truly non-urgent updates as transitions. - Flash of loading state: Very fast resources cause a flicker of the fallback. Use
useTransitionor CSS transitions to avoid sub-second skeleton flashes.
Anti-Patterns
-
Suspense as a loading state manager for local fetches: Using Suspense to handle loading states for
useEffect-based data fetching that does not integrate with Suspense. Only libraries that implement the Suspense protocol (React Query withuseSuspenseQuery, Relay, Next.js) can trigger suspension. A plainfetchinuseEffectwill never suspend. -
One boundary to rule them all: Wrapping the entire application in a single Suspense boundary. This hides all content until the slowest resource resolves, creating a worse user experience than showing a blank page because users see a spinner with no indication of progress.
-
Suspense without an error boundary sibling: Using Suspense to handle the pending state but forgetting to wrap it in an error boundary for the rejected state. When the async resource fails, the error propagates up and crashes the nearest error boundary — or the entire app if there is none.
-
Lazy-loading everything: Applying
React.lazyto every component regardless of size. The overhead of an additional network request and Suspense boundary outweighs the bundle savings for small components. Lazy-load routes and heavy feature modules, not utility components. -
Transitions on urgent user input: Wrapping text input
onChangehandlers instartTransition, which defers the update and makes typing feel laggy. Transitions are for navigation, tab switches, and filter changes — not for keystrokes or other interactions where immediate feedback is essential.
Install this skill directly: skilldb add react-patterns-skills
Related Skills
Compound Components
Compound component pattern for building flexible, implicitly-shared React component APIs
Context Patterns
React Context patterns for efficient state sharing, provider composition, and avoiding unnecessary re-renders
Custom Hooks
Custom hooks pattern for extracting and reusing stateful logic across React components
Error Boundaries
Error boundary pattern for gracefully catching and recovering from runtime errors in React component trees
Optimistic Updates
Optimistic update patterns for instant UI feedback with server reconciliation and rollback in React
Render Props
Render props pattern for sharing cross-cutting logic through function-as-children and render callbacks