React Suspense
In this page:
What Suspense Does
Suspense lets a component pause rendering and show a fallback UI (like a spinner) instead, until whatever it's waiting for is ready. The most common, stable use of this today is React.lazy() for code-splitting, where Suspense shows a fallback while the component's code is still downloading.
Note: Think of Suspense as a specialized, built-in loading-state mechanism, rather than a general-purpose replacement for all async handling.
Warning: Suspense's fallback prop is required — without it, there's nothing to show while the wrapped content isn't ready.
Example: What Suspense Does
<!DOCTYPE html>
<html>
<head>
<script src="https://unpkg.com/react@18/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
</head>
<body>
<div id="root"></div>
<script type="text/babel">
const LazyComponent = React.lazy(() =>
Promise.resolve({ default: () => <p>I loaded (simulated lazy import)</p> })
);
function App() {
return (
<React.Suspense fallback={<p>Loading...</p>}>
<LazyComponent />
</React.Suspense>
);
}
ReactDOM.createRoot(document.getElementById('root')).render(<App />);
</script>
</body>
</html>
One Fallback for Multiple Suspending Children
A single Suspense boundary can wrap several lazy-loaded (or otherwise suspending) children at once. The fallback shows if ANY of them aren't ready yet, and the actual content only appears once ALL of them have finished loading.
Note: Group related lazily-loaded pieces under one Suspense boundary when you want them to appear together, rather than popping in one at a time.
Warning: If one child takes much longer than the others, the whole group stays on the fallback until the slowest one finishes — consider separate boundaries if that's not desired.
Example: One Fallback for Multiple Suspending Children
<!DOCTYPE html>
<html>
<head>
<script src="https://unpkg.com/react@18/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
</head>
<body>
<div id="root"></div>
<script type="text/babel">
const ComponentA = React.lazy(() => Promise.resolve({ default: () => <p>A loaded</p> }));
const ComponentB = React.lazy(() => Promise.resolve({ default: () => <p>B loaded</p> }));
function App() {
return (
<React.Suspense fallback={<p>Loading both...</p>}>
<ComponentA />
<ComponentB />
</React.Suspense>
);
}
ReactDOM.createRoot(document.getElementById('root')).render(<App />);
</script>
</body>
</html>
Suspense Is Not a General Async Replacement
Suspense doesn't automatically work with any Promise or arbitrary async code — it requires something specifically built to integrate with it, like React.lazy for components, or a data-fetching library with explicit Suspense support. A plain fetch() inside useEffect doesn't suspend on its own.
Note: For plain data fetching without a Suspense-compatible library, stick to the loading/error state patterns from the useFetch/useEffect tutorials instead.
Warning: Wrapping a component that does a normal useEffect-based fetch in Suspense does nothing useful — Suspense won't detect or wait for that fetch at all.
Example: Suspense Is Not a General Async Replacement
<!DOCTYPE html>
<html>
<head>
<script src="https://unpkg.com/react@18/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
</head>
<body>
<div id="root"></div>
<script type="text/babel">
// This does NOT integrate with Suspense automatically:
function NormalFetcher() {
const [data, setData] = React.useState(null);
React.useEffect(() => { setData("loaded"); }, []);
return <p>{data || "loading (Suspense doesn't see this)"}</p>;
}
function App() {
return <React.Suspense fallback={<p>This fallback won't show for the above</p>}><NormalFetcher /></React.Suspense>;
}
ReactDOM.createRoot(document.getElementById('root')).render(<App />);
</script>
</body>
</html>
- Trying to use Suspense for regular data fetching (like a fetch() call in useEffect) — that's not what it's built for without a Suspense-compatible data library.
- Forgetting the fallback prop is required — Suspense needs to know what to show while waiting.
- Not understanding Suspense only works with things specifically designed to suspend (like React.lazy or Suspense-enabled data libraries), not arbitrary async code.
- Suspense lets a component tree show a fallback UI while something inside it isn't ready yet.
- The most common, stable use case is code-splitting with React.lazy, showing a fallback while a chunk downloads.
- Suspense for data fetching exists but requires a library specifically built to support it (like React Query's or Relay's Suspense integration).
- A single Suspense boundary can wrap multiple lazy/suspending children, showing one shared fallback.
Suspense for lazy-loaded components stable since React 16.6; Suspense for data fetching is a more recent, evolving feature (React 18+).
Chapter Quiz — Complete all 14 topics to unlock
0/14 topics done
Complete these topics first:
- Error Boundaries
- React Portals
- Modals using Portals (practical use)
- React Suspense
- Code Splitting with React.lazy
- Introduction to Server Components
- Introduction to Next.js (server-side React)
- Using React with TypeScript
- Scalable Folder Architecture
- Common React Design Patterns
- Component Documentation with Storybook
- Accessibility (a11y) in React
- i18n with react-i18next
- React Security Best Practices