Code Splitting and Suspense
|
This section documents React 19.x for the web, using function components and Hooks throughout (class components appear only where React still requires them, such as error boundaries). React Native is out of scope and gets only a pointer. The React 19 additions follow the current official React documentation. This content was generated with the assistance of AI and should be verified against react.dev before being relied on in production, since React APIs continue to evolve between releases. This section’s bibliography lists the reference material consulted while preparing these pages. |
Code splitting breaks the bundle into chunks loaded on demand, so the first screen ships less JavaScript.
React’s tools for this are lazy and <Suspense>.
lazy + <Suspense>
lazy turns a dynamic import() into a component. <Suspense> shows a fallback while that component (or
its data) is still loading.
import { lazy, Suspense } from 'react';
const Settings = lazy(() => import('./Settings.jsx'));
function App() {
return (
<Suspense fallback={<Spinner />}>
<Settings />
</Suspense>
);
}
-
Put a boundary where a meaningful chunk of UI can be replaced by a spinner without a jarring layout shift — often at the route level, sometimes around a heavy widget (a chart, an editor).
-
One
<Suspense>can wrap several lazy children; they share the one fallback. -
Avoid
lazyfor tiny components or anything needed for the first paint — the extra request costs more than it saves.
Route-based splitting is the highest-value case: each route’s component is lazy, so visiting /reports
downloads only the reports chunk. Most routers (React Router, TanStack Router) build this in — see
Routing.
References: lazy,
<Suspense>.
Suspense for data and the use API
<Suspense> also handles data loading when a Suspense-enabled source is read during render: a framework
loader, a Suspense-integrated library, or the use API reading a promise.
import { use, Suspense } from 'react';
function Message({ promise }) {
const text = use(promise); // suspends until the promise resolves
return <p>{text}</p>;
}
// the promise must be STABLE across renders -- from a cache or a framework loader,
// created once (here at module scope), never `fetchGreeting()` inline in the JSX below
const greetingPromise = fetchGreeting();
function Greeting() {
return (
<Suspense fallback={<p>Loading…</p>}>
<Message promise={greetingPromise} />
</Suspense>
);
}
Unlike Hooks, use may be called inside conditions and loops. Creating the promise fresh in render (e.g.
<Message promise={fetchGreeting()} />) makes a new promise every render and re-suspends forever — it must
come from a cache or a framework. use can also read context. See
use.
Error boundaries
<Suspense> handles the pending state; an error boundary handles the failed state. An error boundary
is the one component that still must be a class, because it uses static getDerivedStateFromError and
componentDidCatch.
import { Component } from 'react';
class ErrorBoundary extends Component {
state = { hasError: false };
static getDerivedStateFromError() { return { hasError: true }; }
componentDidCatch(error, info) { logError(error, info); }
render() {
return this.state.hasError ? this.props.fallback : this.props.children;
}
}
<ErrorBoundary fallback={<p>Something went wrong.</p>}>
<Suspense fallback={<Spinner />}>
<Settings />
</Suspense>
</ErrorBoundary>
The react-error-boundary package provides a ready-made
component with retry support. See
Catching rendering
errors with an error boundary.
A Suspense boundary resolving
suspending child"] --> B["Child suspends
(throws a promise)"] B --> C["Nearest <Suspense>
shows fallback"] C --> D["Promise resolves
(chunk / data ready)"] D --> E["React retries the render
fallback swapped for content"] B -.->|"promise rejects"| F["Nearest error boundary
shows its fallback"]