React Suspense: Work better with async data

Nishil Patel

Nishil Patel

Feb 7, 2024

5 min read

Share

React Suspense: Work better with async data

ReactJS Suspense lets you control how your components render while waiting for data or code to load. You can show placeholders, unveil components progressively, display cached content, signal transitions, handle errors, and whatnot, to make your React app even more performant. In this article, you are gonna learn how Suspense can help you work better with async data.

Table of Contents

1.

What is React Suspense?

2.

What problem does React Suspense solve?

3.

What are the common scenarios for using Suspense?

4.

How to use React Suspense for lazy loading?

5.

How to use React Suspense for data fetching?

6.

What are the top benefits of using React Suspense?

7.

What are the updates added for React Suspense in React 18?

8.

FAQs

What is React Suspense?

Suspense is a built-in React component that acts as a wrapper for the child components to “hold off” or “suspend” their rendering with a fallback UI until an action has finished. These actions could be data getting fetched, code being loaded (like lazy-loaded components), or any other async operations. While the child components are waiting to show, you can display a fallback UI, such as a loading spinner, a skeleton UI, or any other placeholder you define. 

<Suspense fallback={<Spinner />}>
  <MyComponent />
</Suspense>

React Suspense was initially introduced with React version 16.6, primarily for code-splitting with React.lazy. It can work with any data fetching method, as long as it can signal its loading state by throwing a promise (though in modern React 18+, the use Hook simplifies this greatly).

React Suspense isn’t a data-fetching library like Axios, or a state management library like Redux or Redux Toolkit. It’s a method that lets you express how your UI components depend on async data or code, and how to handle the loading state in a clear, declarative way.

What problem does React Suspense solve?

One of the trickier parts of building apps is handling async network requests and large JavaScript bundles. It can be frustrating when there are delays, errors, and inconsistencies in the UI while data is being fetched from an API, a database, or a file, or when the entire app code needs to be downloaded before anything is displayed. This can negatively affect the user experience and the performance of the app.

Before React Suspense, the data fetching behavior was usually managed by third-party state management libraries or by manually handling loading states with isLoading booleans and conditional rendering. Similarly, code splitting often required a more complex setup. While these methods have their benefits, the boilerplate and the data/code handling workflows across multiple modules or files can be long, complicated, and prone to errors.

React introduced Suspense to beautifully tackle this problem and offer a more streamlined, declarative way to handle async data and code loading states, deeply integrated into React's rendering model.

What are the common scenarios for using Suspense?

You can use React Suspense to:

Show a fallback UI 

Showing a standby UI while data or code is loading up in the background makes things smoother for the user. It gives them a heads-up that something’s happening, keeps the layout from jumping around, and chills out any worries they might have.

Unveil nested components progressively

The main part of the app shows up first, and the smaller parts come in as they get loaded up. This speeds up your app and keeps the screen from getting stuck.

Display cached content while fresh content loads

The cached data sticks around until the new data comes in. This way, you avoid any loading screens and the screen doesn’t flicker.

Also Read: How to clear DNS Cache on Chrome using chrome://net-internals/#dns

Signal a transition

You can pop up a special transition screen when the user moves around or does something that needs fresh data or code. This makes it clear what’s going on and cuts down on any mix-ups.

Offer a fallback for server errors and client-only content

You can also handle situations where pulling in the data doesn’t work out or the data’s gone missing on the server. This keeps your app from crashing and shows a fitting error message or a standby screen (often in conjunction with an Error Boundary).

Simplify React Debugging with Visual Bug Reports, Console Logs, Network Logs, and More

How to use React Suspense for lazy loading?

Lazy loading, also known as code splitting, allows you to defer the loading of certain parts of your app’s code until they are actually needed. This significantly reduces the initial bundle size and improves the app’s startup performance. React Suspense, in conjunction with React.lazy, provides a built-in way to do this.

Here’s how you can use React Suspense for lazy loading:

Using React.lazy() to dynamically import your component 

Instead of a regular import statement, you'd use React.lazy() with a dynamic import() call. This tells React that this component's code should be loaded only when it's rendered.

// MyComponent.js file

function MyComponent() {
  return <div>This component was lazy loaded!</div>;
}
export default MyComponent; // Must be a default export

// App.js file
import React, { Suspense, lazy } from "react";

// Lazy load MyComponent
const LazyLoadedComponent = lazy(() => import("./MyComponent"));

function App() {
  const [showComponent, setShowComponent] = React.useState(false);

  return (
    <div>
      <h1>App</h1>
      <button onClick={() => setShowComponent(!showComponent)}>
        {showComponent ? "Hide Component" : "Show Component"}
      </button>

      {showComponent && (
        // Wrap the lazy-loaded component with Suspense
        <Suspense fallback={<div>Loading component...</div>}>
          <LazyLoadedComponent />
        </Suspense>
      )}
    </div>
  );
}

export default App;

In this example, the code for MyComponent.js will only be fetched from the network when showComponent becomes true and LazyLoadedComponent attempts to be rendered. While it's loading, "Loading component..." will be displayed. Once loaded, LazyLoadedComponent will appear.

This pattern is perfect for:

  • Route-based code splitting — Loading entire pages or sections only when a user navigates to that route (often combined with React Router).
  • Modal/Dialog components — Deferring the loading of complex modal content until the user clicks to open it.
  • Components below the fold — Loading components that are not immediately visible on the initial screen only when the user scrolls down.

How to use React Suspense for data fetching?

You can use the <Suspense> component to wrap around components that rely on async data or code. You’ll need to provide a fallback prop that specifies what should be rendered while the data or code is loading. Your data fetching logic (or a library supporting Suspense) should signal to React when data isn't ready. 

Before React 18, you’d write code that threw a promise when the data wasn’t ready yet, and resolved it when the data was available. However, with React 18+, the use Hook greatly simplifies this and is the recommended way to include Suspense with data fetching libraries. Plus, in a real app, you'd typically use a Suspense-enabled data fetching library or the use Hook (covered in the React 18 updates in the later section) which handles this complexity for you.

Let's look at a basic example of how a component could signal to Suspense that it's waiting for data.

Basic example of data fetching with Suspense (without the use Hook)

This example shows the basics of how Suspense works by "throwing a promise".

import React, { Suspense } from "react";

// For instance, consider the following as a helper
// from a data-fetching library.
// It tries to read data from a cache. If not found, it "suspends"
// by throwing a promise that resolves when data is fetched.

const fetchDataFromApi = () => {
  // This is a highly simplified example!
  // In a real app, this would interact with a cache or fetcher.

  let data = null;
  let promise = null;

  // Simulate data not being ready initially
  if (!data) {
    promise = new Promise((resolve) => {
      setTimeout(() => {
        data = "Hello from fetched data!"; // Data is now ready
        resolve();
      }, 3000); // Simulate 3-second fetch time
    });
    throw promise; // Suspend the component
  }

  return data;
};

// A component that needs fetched data
function DataDisplayComponent() {
  const data = fetchDataFromApi(); // This call will "suspend"

  return <h2>{data}</h2>;
}

// The App component wraps DataDisplayComponent with Suspense
function App() {
  return (
    <div className="App">
      <h1>Data App</h1>
      <Suspense fallback={<div>Loading data...</div>}>
        <DataDisplayComponent />
      </Suspense>
    </div>
  );
}

export default App;

When the data is ready, the DataDisplayComponent resumes rendering and Voila! It displays the fetched content.

What are the top benefits of using React Suspense?

Using Suspense in React packs several benefits for you to improve the user experience and performance of your React app. Here are some of them:

Declarative and consistent loading states 

React Suspense lets you specify the loading state of your components in a declarative way, without having to use conditional rendering or manual state variables for loading flags. It also ensures that the loading state is consistent across the component tree, by rendering the fallback UI only when the data is not available for all the components in the Suspense boundary.

Smoother transitions and animations 

React Suspense, especially when combined with useTransition, allows you to control the timing and sequencing of your component transitions. You can specify how long to wait before showing the fallback UI, and how long to keep showing the fallback UI after the data is ready. This way, you can avoid showing unnecessary spinners or flashes of content and create smooth and seamless transitions and animations for your components.

Error handling and retry logic

React Suspense itself doesn't directly handle errors, but it's designed to work hand-in-hand with Error Boundaries. The <ErrorBoundary> component is similar to the <Suspense> component, but it catches errors (including those thrown by promises that reject) and renders a fallback UI that can display an error message or a retry button. You typically wrap your <Suspense> component with an <ErrorBoundary> to catch any data fetching or rendering errors.

Concurrent Mode compatibility (now Concurrent Features) 

React Suspense is developed to work with the concurrent features of React (formerly "Concurrent Mode"), which enable React to render your components in a non-blocking way and prioritize the updates that are most important for the user experience. Suspense integrates with these concurrent features and allows you to take advantage of them.

Time slicing, automatic batching, and selective hydration are some of the features that React offers to improve the performance and user experience of web apps. 

Here’s a brief explanation for each:

  • Time slicing — React breaks rendering work into small pieces and does them when idle, without blocking the main thread or user input.
  • Automatic batching — React combines multiple state updates (even those outside of event handlers) into one render, saving re-renders and making the app faster.
  • Selective hydration — React hydrates (links HTML to JavaScript) the most critical parts of the app first, and delays or skips the rest, making interactive content available sooner.

What are the updates added for React Suspense in React 18?

The core concepts of Suspense remain true for all supported versions (starting from version 16.6), but with the stable release of React 18, its integration into the ecosystem has matured significantly, especially for data fetching.

Here are the key updates:

From Concurrent Mode to Concurrent Features 

In React 18 and later, "Concurrent Mode" is no longer an experimental feature. Instead, Concurrent Features are now built-in and enabled by default when you use createRoot. You no longer opt into a special "mode". 

Features like time slicing, automatic batching, and transitions are now integral to how React works. This means Suspense is not just an add-on but a core piece of the React rendering model, working seamlessly to enable non-blocking rendering and smoother user interfaces.

The use Hook: A simpler way to suspend 

While throwing a promise from a component is the underlying mechanism that makes Suspense work, React now provides a much simpler and cleaner API for this: the use Hook

The use Hook can be called inside Render Components and Hooks to read the value of a resource, such as a Promise or a context. When you pass a promise to use, it handles the "suspending" for you:

  • If the promise is pending, the use Hook signals to React that the component needs to suspend.
  • If the promise has resolved, the use Hook returns the resolved value.
  • If the promise has rejected, the use Hook throws the rejection reason, which can be caught by a nearby Error Boundary.

Here is how a component using data can be simplified with the use Hook:

Basic Example with the use Hook:

import { Suspense, use } from "react";

// Imagine this promise comes from a data fetching mechanism.
// It will resolve with some data after a delay.
const myDataPromise = new Promise((resolve) => {
  setTimeout(() => {
    resolve("Data is here!");
  }, 3000); // Simulate 3-second fetch
});

function MyComponentWithData() {
  // The 'use' hook unwraps the promise.
  // If the promise is pending, this will "suspend" the component.
  const data = use(myDataPromise);

  return <h2>{data}</h2>;
}

function App() {
  return (
    <div>
      <h1>App with Data</h1>
      <Suspense fallback={<div>Loading data with use hook...</div>}>
        <MyComponentWithData />
      </Suspense>
    </div>
  );
}

export default App;

React Server Components and Streaming 

One of the most significant advancements related to Suspense is the introduction of React Server Components (RSC). With Server Components, data fetching can happen on the server before the component even renders on the client. 

Suspense plays a critical role here by allowing you to stream HTML from the server to the client.

  • You can wrap a slow server component in a <Suspense> boundary.
  • The server will send the fallback UI (e.g., a skeleton loader) immediately as part of the initial HTML.
  • Once the server component finishes fetching its data and renders, React streams the resulting HTML to the client, which then seamlessly swaps the fallback with the final content. This pattern drastically improves the initial page load time and user experience, as the user sees a meaningful layout instantly instead of a blank screen.

Managing Transitions and Pending States with useTransition 

The useTransition Hook has become a key part of the modern Suspense workflow, especially for handling UI changes during data mutations (like form submissions) or navigation. useTransition lets you mark a state update as a "transition," which tells React that it's not urgent and can be interrupted. 

When used with Suspense, it allows you to show pending feedback without replacing the existing UI. For example, when a user clicks a button to refresh data:

  • Without useTransition, the entire component might disappear and be replaced by a fallback spinner, which can be a jarring experience.
  • With useTransition, you can keep the old (stale) data visible while showing a subtle pending indicator elsewhere on the screen. The isPending value returned by the hook makes this easy to implement. This is crucial for Actions, a pattern where server-side data mutations are initiated from the client. useTransition helps manage the pending and optimistic UI states for these actions, working hand-in-hand with Suspense to handle loading states gracefully.

FAQs

You need to use the startTransition function and the useTransition hook from React. The startTransition function lets you mark a state update as a transition, meaning it can be delayed or interrupted by a higher-priority update. The useTransition hook lets you access the transition state and render a fallback UI while the transition is pending.

Written by

Nishil Patel | CEO & Founder

Follow

Nishil is a successful serial entrepreneur. He has more than a decade of experience in the software industry. He advocates for a culture of excellence in every software product.

Subscribe to our updates

Meet the Author: Nishil Patel, CEO, and Co-founder of BetterBugs. With a passion for innovation and a mission to improve software quality.

We never spam.

Nothing here!
Show us some love 💖. Try BetterBugs today if you haven’t already. A quick feedback and a rating on our Chrome web store page would be awesome!

Share your experience with the founderhere!

Don’t wait! Start reporting now.