# What is Suspense
is a React component that “displays a fallback until its children have finished loading”. In other words, any child component that suspends (due to lazy-loaded code or pending data) will cause React to pause rendering of that tree and show the fallback UI instead. This enables asynchronous rendering patterns without manual loading state logic. Under the hood, React 18+ runs in concurrent mode so it can delay (suspend) rendering parts of the component tree.
- Fallback UI: A lightweight placeholder (spinner, skeleton, etc.) shown when children suspend. Suspense-enabled sources: Only Suspense-enabled operations trigger this.
- Examples include code-splitting (
React.lazy), data frameworks (Relay, SWR, Next.js), or the newuse()hook for Promises. Fetching data inside auseEffector event handler will not trigger Suspense.
import React, { Suspense } from 'react'
function App() {
return (
<div>
<h1>My App</h1>
{/* Any component inside Suspense can “suspend” */}
<Suspense fallback={<div>Loading...</div>}>
<SomeComponent /> {/* might be lazy-loaded or fetch data */}
</Suspense>
</div>
)
}Here, SomeComponent could be lazily loaded or await a data promise. React will automatically render the fallback until SomeComponent is ready. Note that Suspense only works if the suspended component uses a supported async source (e.g. a dynamic import, a Promise via use(), or a Suspense-enabled library).
# Lazy Loading (Code Splitting)
React’s lazy() API and Suspense enable route- and component-level code splitting. Use React.lazy() to dynamically import a component only when it’s needed. For example:
import React, { Suspense, lazy } from 'react'
// Lazy import (component loads in a separate chunk when rendered)
const HeavyComponent = lazy(() => import('./HeavyComponent'))
function App() {
return (
<div>
<h1>Main App</h1>
{/* Suspense shows fallback while HeavyComponent is fetched */}
<Suspense fallback={<div>Loading heavy component...</div>}>
<HeavyComponent /> {/* Only loads on first render */}
</Suspense>
</div>
)
}In this example, HeavyComponent is not part of the initial bundle. It will load only when React renders it inside . This reduces initial load size and improves performance — and when paired with React Compiler's automatic memoization, the re-render performance of these lazy-loaded components is optimized automatically. Remember to always include a fallback; without it, the lazy-loaded component would throw an error. Lazy loading can be applied to any component (not just routes), but it’s most common for pages or rarely used parts of the app.
# Example: Lazy Loading with React Router
React Router works seamlessly with React.lazy and Suspense to lazy-load pages. For example, in React Router v6+:
import { BrowserRouter, Routes, Route } from 'react-router-dom'
const Home = React.lazy(() => import('./Home'))
const About = React.lazy(() => import('./About'))
function App() {
return (
<BrowserRouter>
{/* Wrap Routes with Suspense for route-level code splitting */}
<Suspense fallback={<p>Loading page...</p>}>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
</Routes>
</Suspense>
</BrowserRouter>
)
}Here, each route component ( Loading page...Home and About) is loaded only when the user navigates to that path. As Partha Roy notes, “React Router works well with lazy and suspense. You can use these together to load route-based components”. The fallback is shown until the lazy route component finishes downloading. This approach keeps the initial JS bundle small and loads pages on-demand.
# Data Fetching with Suspense
Beyond code splitting, Suspense can manage asynchronous data as well. With React 18 it was experimental, but React 19 introduces the new use() hook to suspend on Promises natively. Instead of useEffect and manual loading flags, you can simply pass a Promise to use() inside a Suspense boundary. For example:
import React, { use, Suspense } from 'react'
async function fetchTodo(id: number) {
const res = await fetch(`https://jsonplaceholder.typicode.com/todos/${id}`)
return res.json()
}
// Parent component (no async/await here – we create a promise)
function App() {
const todoPromise = fetchTodo(1) // create promise once
return (
<Suspense fallback={<div>Loading data...</div>}>
<Todo titlePromise={todoPromise} />
</Suspense>
)
}
// Client component with use()
function Todo({ titlePromise }: { titlePromise: Promise<{ title: string }> }) {
const data = use(titlePromise)
return <h1>{data.title}</h1> // Renders when promise resolves
}In this pattern, suspends on the titlePromise. While it’s pending, React shows the loading fallback. Once resolved, use() returns the data and is rendered. If the promise rejects, the nearest Error Boundary will catch it (see next section).