What is React Suspense?
React Suspense is a feature that lets you "suspend" the rendering of your component tree until some asynchronous operation - such as loading data, fetching resources, or loading a component - is completed.
In this blog post, we will explore how React Suspense works, discuss its benefits, and show how to use it through practical code snippets.
React Suspense and Server-Side Rendering (SSR)
React Suspense improves server-side rendering (SSR) by enhancing both performance and user experience. Instead of waiting for the entire page to load, you can use renderToPipeableStream()
to prioritize key content, allowing users to see important parts of the page immediately. The remaining content loads progressively, with suspense handling fallbacks smoothly.
This approach not only speeds up rendering but also boosts SEO by ensuring faster delivery of essential content, making it easier for search engines to crawl and index your site.
Here’s a basic implementation:
import { renderToPipeableStream } from 'react-dom/server';
import App from './App';
renderToPipeableStream(<App />, {
onShellReady() {
// send critical content first
},
onAllReady() {
// send the rest of the content progressively
}
});
How React Suspense Works
React Suspense operates by allowing components to "suspend" while waiting for asynchronous operations to complete, such as data fetching or lazy-loaded components. The process works as follows:
1. Initial Render: When the component tree is rendered, React evaluates each component wrapped in a suspense boundary.
2. Suspension Detection: If a child component within the suspense boundary is still waiting for data (e.g., due to a React.lazy()
import or a fetch request), React detects that it is in a suspended state.
3. Displaying Fallback UI: While the data is being fetched or the component is being lazy-loaded, React automatically displays the fallback UI specified in the suspense component, like a loading spinner or placeholder.
4. Rendering the Content: Once the asynchronous task completes (i.e., the data is fetched or the component is loaded), React transitions smoothly from the fallback UI to rendering the actual content.
React Suspense Props
When using React Suspense, there are two main props that you need to understand: children
and fallback
. These props are crucial in managing what gets rendered during the loading of asynchronous components or data.
children
: Thechildren
prop refers to the actual UI that you want to render within the suspense component. It represents the part of your application that may involve an asynchronous operation, such as loading a component or fetching data. If thechildren
component suspends while rendering (i.e., it's still waiting for an asynchronous task to complete), the suspense boundary automatically switches to rendering thefallback
component.fallback
: Thefallback
prop is the UI that will be rendered temporarily while thechildren
is still loading. It acts as a placeholder, typically displaying a loading spinner, skeleton screen, or any other lightweight component. Thefallback
UI ensures that your users are not left staring at a blank screen while waiting for the content to load.
React Features and Use Cases
1. Handling Fallbacks During Content Loading
You can wrap any part of your application with a suspense boundary to manage loading states:
<Suspense fallback={<Loading />}>
<Albums />
</Suspense>
In this setup, React will show the specified fallback (<Loading />
) until all the necessary code and data for the Albums
component has been fetched and is ready to render.
For example, when the Albums
component is fetching the list of albums, it will "suspend," and React will automatically switch to the fallback (the Loading
component). Once the data is fully loaded, the fallback disappears, and the Albums
component is rendered with the retrieved data.
Code: ArtistPage.js
import { Suspense } from 'react';
import Albums from './Albums.js';
export default function ArtistPage({ artist }) {
return (
<>
<h1>{artist.name}</h1>
<Suspense fallback={<Loading />}>
<Albums artistId={artist.id} />
</Suspense>
</>
);
}
function Loading() {
return <h2>🌀 Loading...</h2>;
}
2. Simultaneous Content Loading
By default, the entire component tree inside a suspense boundary is treated as a single unit. This means that if any one component within the boundary is suspended (waiting for data or resources), the entire set of components wrapped by suspense will be replaced by the loading indicator:
<Suspense fallback={<Loading />}>
<Biography />
<Panel>
<Albums />
</Panel>
</Suspense>
In this example, both Biography
and Albums
components may fetch data, but if either suspends, React will display the fallback (the Loading
component) for both. Only when all the data is fully loaded will both components "pop in" together and be displayed at once.
Code: ArtistPage.js
import { Suspense } from 'react';
import Albums from './Albums.js';
import Biography from './Biography.js';
import Panel from './Panel.js';
export default function ArtistPage({ artist }) {
return (
<>
<h1>{artist.name}</h1>
<Suspense fallback={<Loading />}>
<Biography artistId={artist.id} />
<Panel>
<Albums artistId={artist.id} />
</Panel>
</Suspense>
</>
);
}
function Loading() {
return <h2>🌀 Loading...</h2>;
}
Code: Panel.js
export default function Panel({ children }) {
return (
<section className="panel">
{children}
</section>
);
}
3. Progressive Loading of Nested Content
When a component suspends, the nearest React Suspense parent displays its fallback. By nesting multiple suspense components, you can create a sequential loading experience. As each nested component loads, its fallback is replaced by the actual content.
For instance, you can give the Albums
component its own fallback, so other parts of the UI don’t need to wait for Albums
to load:
<Suspense fallback={<BigSpinner />}>
<Biography />
<Suspense fallback={<AlbumsGlimmer />}>
<Panel>
<Albums />
</Panel>
</Suspense>
</Suspense>
With this setup:
- If
Biography
is still loading, theBigSpinner
is shown for the whole content. - Once
Biography
finishes loading, the spinner is replaced with theBiography
component. - If
Albums
is still loading, theAlbumsGlimmer
is shown inside thePanel
. - Finally, when
Albums
loads, it replacesAlbumsGlimmer
.
This approach allows different parts of the UI to load independently, improving the overall user experience by avoiding long waits for unrelated content.
Code: ArtistPage.js
import { Suspense } from 'react';
import Albums from './Albums.js';
import Biography from './Biography.js';
import Panel from './Panel.js';
export default function ArtistPage({ artist }) {
return (
<>
<h1>{artist.name}</h1>
<Suspense fallback={<BigSpinner />}>
<Biography artistId={artist.id} />
<Suspense fallback={<AlbumsGlimmer />}>
<Panel>
<Albums artistId={artist.id} />
</Panel>
</Suspense>
</Suspense>
</>
);
}
function BigSpinner() {
return <h2>🌀 Loading...</h2>;
}
function AlbumsGlimmer() {
return (
<div className="glimmer-panel">
<div className="glimmer-line" />
<div className="glimmer-line" />
<div className="glimmer-line" />
</div>
);
}
Code: Panel.js
export default function Panel({ children }) {
return (
<section className="panel">
{children}
</section>
);
}
4. Displaying Stale Content During Fresh Data Loading
In scenarios where content is being reloaded due to changes (such as updating a search query), React Suspense can momentarily replace the current content with a loading fallback while new data is fetched.
For instance, consider a SearchResults
component that fetches and displays search results. When you type "a" and wait for the results, React shows the results for "a." However, when you edit the query to "ab," the component will suspend while fetching the updated results. During this time, the existing results for "a" will be replaced by a loading fallback until the new results for "ab" are ready.
Code: App.js
import { Suspense, useState } from 'react';
import SearchResults from './SearchResults.js';
export default function App() {
const [query, setQuery] = useState('');
return (
<>
<label>
Search albums:
<input value={query} onChange={e => setQuery(e.target.value)} />
</label>
<Suspense fallback={<h2>Loading...</h2>}>
<SearchResults query={query} />
</Suspense>
</>
);
}
5. Preserving Visible Content During Loading
By default, when a component suspends, the closest suspense boundary switches to the fallback, which can replace the entire visible UI. This can feel jarring if part of the content has already been displayed and suddenly disappears, replaced by a loading indicator.
For example, imagine you press a button that navigates from IndexPage
to ArtistPage
. If a component inside ArtistPage
suspends while fetching data, the nearest suspense boundary activates and shows the fallback. If that boundary is near the root of your app, such as the site’s main layout, it can replace everything with a loading spinner (BigSpinner
), creating a disruptive experience.
To avoid this, you can structure your suspense boundaries more carefully. By placing suspense boundaries closer to the specific components that might suspend, you prevent the entire page from being replaced. This ensures that previously displayed content stays visible while only the new, suspended content shows a fallback.
Code: App.js
import { Suspense, useState } from 'react';
import IndexPage from './IndexPage.js';
import ArtistPage from './ArtistPage.js';
import Layout from './Layout.js';
export default function App() {
return (
<Suspense fallback={<BigSpinner />}>
<Router />
</Suspense>
);
}
function Router() {
const [page, setPage] = useState('/');
function navigate(url) {
setPage(url);
}
let content;
if (page === '/') {
content = (
<IndexPage navigate={navigate} />
);
} else if (page === '/the-beatles') {
content = (
<ArtistPage
artist={{
id: 'the-beatles',
name: 'The Beatles',
}}
/>
);
}
return (
<Layout>
{content}
</Layout>
);
}
function BigSpinner() {
return <h2>🌀 Loading...</h2>;
}
Code: Layout.js
export default function Layout({ children }) {
return (
<div className="layout">
<section className="header">
Music Browser
</section>
<main>
{children}
</main>
</div>
);
}
Code: IndexPage.js
export default function IndexPage({ navigate }) {
return (
<button onClick={() => navigate('/the-beatles')}>
Open The Beatles artist page
</button>
);
}
Code: ArtistPage.js
import { Suspense } from 'react';
import Albums from './Albums.js';
import Biography from './Biography.js';
import Panel from './Panel.js';
export default function ArtistPage({ artist }) {
return (
<>
<h1>{artist.name}</h1>
<Biography artistId={artist.id} />
<Suspense fallback={<AlbumsGlimmer />}>
<Panel>
<Albums artistId={artist.id} />
</Panel>
</Suspense>
</>
);
}
function AlbumsGlimmer() {
return (
<div className="glimmer-panel">
<div className="glimmer-line" />
<div className="glimmer-line" />
<div className="glimmer-line" />
</div>
);
}
6. Visual Feedback for Transitions
In certain scenarios, such as navigating between pages, there might be no visual indication that a transition is in progress. This can lead to confusion for users who expect feedback during navigation. To address this, you can use useTransition
in React, which provides a boolean value isPending
to indicate whether a transition is currently happening.
For instance, by replacing startTransition
with useTransition
, you gain access to the isPending
state. This state can be used to update the UI dynamically during transitions, such as changing the style of the website's header or showing a subtle loading indicator.
Here’s how it works:
- When you trigger a navigation (e.g., clicking a button), the
useTransition
hook setsisPending
to true. - While
isPending
istrue
, you can change the visual styling, like dimming the header or displaying a progress bar. - Once the transition completes and the new content is loaded,
isPending
returns tofalse
, and the original styling is restored.
Code: App.js
import { Suspense, useState, useTransition } from 'react';
import IndexPage from './IndexPage.js';
import ArtistPage from './ArtistPage.js';
import Layout from './Layout.js';
export default function App() {
return (
<Suspense fallback={<BigSpinner />}>
<Router />
</Suspense>
);
}
function Router() {
const [page, setPage] = useState('/');
const [isPending, startTransition] = useTransition();
function navigate(url) {
startTransition(() => {
setPage(url);
});
}
let content;
if (page === '/') {
content = (
<IndexPage navigate={navigate} />
);
} else if (page === '/the-beatles') {
content = (
<ArtistPage
artist={{
id: 'the-beatles',
name: 'The Beatles',
}}
/>
);
}
return (
<Layout isPending={isPending}>
{content}
</Layout>
);
}
function BigSpinner() {
return <h2>🌀 Loading...</h2>;
}
Code: Layout.js
export default function Layout({ children, isPending }) {
return (
<div className="layout">
<section className="header" style={{
opacity: isPending ? 0.7 : 1
}}>
Music Browser
</section>
<main>
{children}
</main>
</div>
);
}
Code: IndexPage.js
export default function IndexPage({ navigate }) {
return (
<button onClick={() => navigate('/the-beatles')}>
Open The Beatles artist page
</button>
);
}
Code: ArtistPage.js
import { Suspense } from 'react';
import Albums from './Albums.js';
import Biography from './Biography.js';
import Panel from './Panel.js';
export default function ArtistPage({ artist }) {
return (
<>
<h1>{artist.name}</h1>
<Biography artistId={artist.id} />
<Suspense fallback={<AlbumsGlimmer />}>
<Panel>
<Albums artistId={artist.id} />
</Panel>
</Suspense>
</>
);
}
function AlbumsGlimmer() {
return (
<div className="glimmer-panel">
<div className="glimmer-line" />
<div className="glimmer-line" />
<div className="glimmer-line" />
</div>
);
}
7. Providing a Fallback for Server Errors and Client-Only Content
React Suspense helps manage server-side errors by showing fallback UI (e.g., a loading spinner) when a component fails during server rendering. Instead of terminating the render, React finds the nearest suspense boundary and displays the fallback, preventing broken pages. On the client, React retries rendering the component. If it succeeds, the fallback is replaced with the actual content.
You can also mark components as client-only by throwing an error on the server, like this:
<Suspense fallback={<Loading />}>
<Chat />
</Suspense>
function Chat() {
if (typeof window === 'undefined') {
throw new Error('Chat should only render on the client.');
}
// ...
}
This approach ensures smooth transitions between server and client rendering, while avoiding server-side issues.
Conclusion
React Suspense simplifies asynchronous rendering in React by handling loading states and fallbacks efficiently. It helps ensure smoother transitions, manage nested content loading, and reset boundaries during navigation, all while improving user experience. Whether you're working with lazy loading or data fetching, Suspense keeps your UI responsive and clean. For more details, check the official React Suspense documentation.