React Suspense: A Complete Guide

Sarthak Niranjan - Sep 15 - - Dev Community

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 Image

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
  }
});

Enter fullscreen mode Exit fullscreen mode

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.

  1. children : The children 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 the children component suspends while rendering (i.e., it's still waiting for an asynchronous task to complete), the suspense boundary automatically switches to rendering the fallback component.

  2. fallback : The fallback prop is the UI that will be rendered temporarily while the children is still loading. It acts as a placeholder, typically displaying a loading spinner, skeleton screen, or any other lightweight component. The fallback 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

Handling Fallbacks Image

You can wrap any part of your application with a suspense boundary to manage loading states:

<Suspense fallback={<Loading />}>
  <Albums />
</Suspense>
Enter fullscreen mode Exit fullscreen mode

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>;
}
Enter fullscreen mode Exit fullscreen mode

2. Simultaneous Content Loading

Simultaneous Content Loading Image

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>
Enter fullscreen mode Exit fullscreen mode

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>;
}
Enter fullscreen mode Exit fullscreen mode

Code: Panel.js

export default function Panel({ children }) {
  return (
    <section className="panel">
      {children}
    </section>
  );
}
Enter fullscreen mode Exit fullscreen mode

3. Progressive Loading of Nested Content

Progressive Loading of Nested Content Image

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>
Enter fullscreen mode Exit fullscreen mode

With this setup:

  1. If Biography is still loading, the BigSpinner is shown for the whole content.
  2. Once Biography finishes loading, the spinner is replaced with the Biography component.
  3. If Albums is still loading, the AlbumsGlimmer is shown inside the Panel.
  4. Finally, when Albums loads, it replaces AlbumsGlimmer.

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

Code: Panel.js

export default function Panel({ children }) {
  return (
    <section className="panel">
      {children}
    </section>
  );
}
Enter fullscreen mode Exit fullscreen mode

4. Displaying Stale Content During Fresh Data Loading

Fresh Data Loading Image

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>
    </>
  );
}
Enter fullscreen mode Exit fullscreen mode

5. Preserving Visible Content During Loading

Preserving Image

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>;
}
Enter fullscreen mode Exit fullscreen mode

Code: Layout.js

export default function Layout({ children }) {
  return (
    <div className="layout">
      <section className="header">
        Music Browser
      </section>
      <main>
        {children}
      </main>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Code: IndexPage.js

export default function IndexPage({ navigate }) {
  return (
    <button onClick={() => navigate('/the-beatles')}>
      Open The Beatles artist page
    </button>
  );
}
Enter fullscreen mode Exit fullscreen mode

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

6. Visual Feedback for Transitions

Visual Feedback Image

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:

  1. When you trigger a navigation (e.g., clicking a button), the useTransition hook sets isPending to true.
  2. While isPending is true, you can change the visual styling, like dimming the header or displaying a progress bar.
  3. Once the transition completes and the new content is loaded, isPending returns to false, 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>;
}
Enter fullscreen mode Exit fullscreen mode

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

Code: IndexPage.js

export default function IndexPage({ navigate }) {
  return (
    <button onClick={() => navigate('/the-beatles')}>
      Open The Beatles artist page
    </button>
  );
}
Enter fullscreen mode Exit fullscreen mode

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

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.');
  }
  // ...
}
Enter fullscreen mode Exit fullscreen mode

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.

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .