Technology · React
React Suspense
Load components and data with Suspense boundaries for better UX and streaming.
TL;DR
- 01Use Suspense to show loading states while components or data load.
- 02Wrap lazy-loaded components in Suspense for fallback UI.
- 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.