The new Suspense API in React 18

What is it? What does it eat? Where does it live?

Ingryd Moura
hurb.labs
9 min readFeb 6, 2023

--

Before all… What is the Suspense API?

An API that can be used to suspense the component execution. It is a way to show a fallback while the component is suspended.

Something like that …

Loading gif.

Why use it?

  • Code splitting
  • User experience
  • No state control to show “loading”.

Code splitting

With React.Lazy, you can import a component dynamically, and it loads if necessary.
The bundle involves aligning the components in order and putting in a javascript code. But, as our application grows, the bundle becomes larger and turns the application slower and harder to use.
The bundle can be divided into smaller pieces with code splitting, and the critical part can be loaded first.

User experience

When applications are created, you need to consider users using slower connections. So, you need to control the user experience during the suspense timer.

No state control to show “loading”.

Does it look familiar to you?

import React, { useState } from 'react';

const Component = () => {
const [isLoading, setIsLoading] = useState(false);

return (
<div>
{isLoading ? <Spinner /> : <p>Hello!</p>}
</div>
)
};

Without the Suspense, you must access the isLoading state to show the “loading”. In this case, show the Spinner component when the state isLoading = true . If not, show the Hello text.

How to use it?

Using the Suspense, you must provide a fallback option (a React component or a string, for example). It can be many levels down in the DOM, and the fallback would show.

import React, { lazy, Suspense } from 'react';

const LazyComments = lazy(() => import('./Comments'));

const Component = () => (
<Suspense fallback={<Spinner />}>
<LazyComments />
</Suspense>
);

In this example, theSpinner component is a fallback option and the Comments is a Lazy component.

Nested Suspense components can be used to improve the load process.

Look it:

const Component = () => (
<Suspense fallback={<RightColumnSpinner />}>
<RightColumn>
<SideBar />
</RightColumn>
<LeftColumn>
<Suspense fallback={<LeftColumnSpinner />}>
<Comments />
<Post />
</Suspense>
</LeftColumn>
</Suspense>
);

If SideBar is suspended, all the page is replaced with RightColumnSpinner, but if Comments or Post is suspended, both are replaced withLeftColumnSpinner .

Legacy Suspense x Concurrent Suspense

There are some differences between Suspense (React 16) and Suspense (React 18):

Comparing Legacy Suspense and Concurrent Suspense. Legacy Suspense: The tree elements are mounted instantly in DOM, the tree is visually hidden when the suspense is triggered, and It throws an error when used in SSR. Concurrent Suspense: The element isn’t mounted until the suspended component is resolved, the elements tree doesn’t exist when suspense is actioned, and it’s working with SSR.
Table image comparing Legacy Suspense and Concurrent Suspense.

Let’s see how it works in React 16 and React 18 with this example:

const Component = () => (
<div>
{showComments && (
<Suspense fallback={<Spinner />}>
<Panel>
<Comments />
</Panel>
</Suspense>
)}
</div>
);

If showComments changes from false to true, React starts to render the Panel content but <Comments /> is suspended. In this case, the <Panel /> doesn’t appear because all content wrapped by the nearest Suspense is hidden until the DOM tree is ready.

So, these are the components that I created above.

The Panel component has the Comments component inside it.
Image of component Panel and inside it the component Comments.

In React 16:

Image of the steps of suspense in React 16.

Steps

  1. Put the <Panel /> in DOM, but with a “space” instead of<Comments />
  2. Add display: none in <Panel /> to hide
  3. Add <Spinner /> in the DOM
  4. Wait <Comments /> until it is ready
  5. Try to render again
  6. Remove <Spinner /> from DOM
  7. Put <Comments /> inside <Panel /> (is still hidden)
  8. Remove display: none from the<Panel />

Now, in React 18:

Image of the steps of suspense in React 18.

Steps

  1. Remove the <Panel /> from the DOM
  2. Add <Spinner /> in the DOM
  3. Wait <Comments /> until it is ready
  4. Try to render again
  5. Remove <Spinner /> from DOM
  6. Put <Panel /> with <Comments /> in the DOM

Suspense in SSR

If you don’t know about SSR(Server Side Rendering) yet, I recommend this explanation to help you understand better.

SSR applications have some steps:

Steps: Fetch data for the application (server), renders the app to HTML and send it in the response (server), load the JS code for the app (client), and connect the JS logic into the HTML generated by the server for the app (client).
Image of the SSR steps.

It is essential to know that each step has to finish the whole application in one go before the next stage starts.
This isn’t efficient if some parts of the app are slower than others.
React 18 allows <Suspense /> split your app into smaller independent units that will go through those steps independently and not block the rest of the app.
As a result, users will see the content sooner and will be able to start interacting with it faster. The slower parts of your app will not “drag” on the faster parts.
This also o means that React.lazy works with SSR now. Before, if a component was suspended during server rendering, an error occurred, so apps couldn’t use Suspense.

SSR problems

  • Problem 1: Search for everything before you can show anything

A problem with SSR today is that it doesn’t allow components to wait for data. So when the HTML renders, you should already have all the data for your components on the server. This means you must collect all the server data before sending any HTML to the client.

For example, we are rendering a post with comments.
The comments are essential to display first, so you want to include them in the HTML output of the server. But your API is slow, and you don’t have control over this.

Now, you have two difficult choices:
- Exclude the data from the server output, but the user won’t see the data until de javascript is loaded.
- Include the data from the server output, but delay sending the rest of the HTML until the comments are loaded and the entire tree can be rendered.

Solution

Wrap part of the code with <Suspense /> .

const Component = () => (
<>
<Navbar />
<Suspense fallback={<Spinner />}>
<Comments />
</Suspense>
</>
);

In this case, React should display the <Spinner /> until the <Comments /> is ready.
By wrapping <Comments /> in <Suspense />, React doesn’t have to wait for the comments to start transmitting the HTML to the rest of the page. Instead, send the placeholder (a spinner) instead of the comments.
Initially, the comments are not in the HTML.
When the comments data is ready on the server, we will send additional HTML to the same stream to put it in the correct position.
As a result, the delayed HTML for comments will appear before React is loaded on the client.
This way, it is unnecessary to fetch all the data before showing anything.
If some part of the screen delays the initial HTML, you don’t need to choose between delaying all HTML or removing it from the server’s HTML. Instead, you can allow that part to appear later in the HTML flow.

  • Problem 2: Load for everything before you can hydrate anything

After that javascript code is loaded, and the HTML will go hydrated, turning interactive. The React will run the HTML generated from the server while rendering the components and attaching event handlers. And to this to work, the DOM tree produced in the browser must correspond to the tree on the server. Otherwise, you can’t match them!
One consequence is that you must load JavaScript for all components in the client before you can start hydrating any of them.

For example, a comment widget has complex interaction logic and takes time to load JavaScript.

It would be great to render the comments on the server for the HTML to show them to the user first. But, the hydration can only be done at one time; we can’t hydrate the navigation bar or sidebar or post content until to load the code for the comment widget.

Solution

To use React.lazy().

import React, { lazy, Suspense } from 'react';

const LazyComments = lazy(() => import('./Comments'));

const Component = () => (
<Suspense fallback={<Spinner />}>
<LazyComments />
</Suspense>
);

We can upload the initial HTML first, but there is a problem: once the code javascript is loaded, it is impossible to start to hydrate the application on the client, and if the size code is large, it can take a while.
You use code splitting to avoid large bundlers and separate the comment code from the main bundler. In this case, you specify the code that doesn’t need to be loaded synchronously, and the bundler will split it into a separate script tag.
Before, this didn’t work in server rendering. Now, in React 18, the Suspense allows hydrating the application before the comments widget is loaded.
When <Comments /> it is wrapped in the <Suspense />, React doesn’t block the rest of the page while it is loading. So you don’t have to wait for all the code to load to start hydrating.
React will start hydrating the comments after the code has finished loading. It is called Selective Hydration Process: when a heavy chunk of javascript doesn’t prevent the rest of the page from becoming interactive.

  • Problem 3: Hydrate for everything before you can interact with anything

When the hydration process starts, React doesn’t stop until it finishes the DOM tree.
Therefore, you must wait for all components to be hydrated before interacting with them.

For example, a comment widget has expensive rendering logic.
A device with low performance can crash the screen for a while. As a result, once hydration starts, the user cannot interact with any content until the entire tree is hydrated. For navigation, this is veeeery bad because the user may want to navigate out of a page, but since React is busy doing hydration, the user is kept on the current page that they don’t care about anymore.

This is because there is an order:
fetch data -> render to HTML -> load code -> hydrate

Each step can start once the previous step has finished. The solution is to separate the work so that it is possible to do each stage for a part of the screen instead of the whole application.

Solution

Use the <Suspense /> in more places. It is possible to split how components are loaded.

For example, when the <Suspense /> is wrapped in the <Comments /> and the <Sidebar />, their hydration no longer prevents the browser from doing another job.

const Component = () => (
<>
<Navbar />
<Suspense fallback={<Spinner />}>
<Sidebar />
</Suspense>
<Panel>
<Post />
<Suspense fallback={<Spinner />}>
<Comments />
</Suspense>
</Panel>
</>
);

Let me show you how this would work in SSR:

The component has the following details in the order: Navbar (top of the page), Sidebar (right side of the page), Post (below the navbar), and Comments (below the post). 1- Navbar and Post loaded, but the Sidebar and Comments aren’t loaded. 2 — Navbar and Post loaded, post (below is not loaded; in other countries, take the Hydaration and hydrating in Sidebar. 3 — Navbar and Post loaded, post (below is not loaded; in other countries, take the Hydaration and hydrating in Sidebar.
Image of the steps of Suspense in SSR.

The HTML is loaded for all components, but the code still needs to be added.

  1. <Navbar /> and <Post /> are loaded code, but <Sidebar /> and <Comments /> components are not yet.
  2. <Sidebar /> starts to hydrate because it is the first in the DOM tree.
  3. But the user clicks in <Comments /> , when <Sidebar /> it is loading.
  4. <Comments /> starts to hydrate instead <Sidebar />.
  5. Now, with the <Comments /> load, <Sidebar /> turn back to hydrate.
  6. All components are loaded.

The React starts hydrating everything early as possible and prioritizes the most urgent part of the screen based on user interaction.

--

--