The new Suspense API in React 18
What is it? What does it eat? Where does it live?
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 …
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):
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.
In React 16:
Steps
- Put the
<Panel />
in DOM, but with a “space” instead of<Comments />
- Add
display: none
in<Panel />
to hide - Add
<Spinner />
in the DOM - Wait
<Comments />
until it is ready - Try to render again
- Remove
<Spinner />
from DOM - Put
<Comments />
inside<Panel />
(is still hidden) - Remove
display: none
from the<Panel />
Now, in React 18:
Steps
- Remove the
<Panel />
from the DOM - Add
<Spinner />
in the DOM - Wait
<Comments />
until it is ready - Try to render again
- Remove
<Spinner />
from DOM - 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:
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 HTML is loaded for all components, but the code still needs to be added.
<Navbar />
and<Post />
are loaded code, but<Sidebar />
and<Comments />
components are not yet.<Sidebar />
starts to hydrate because it is the first in the DOM tree.- But the user clicks in
<Comments />
, when<Sidebar />
it is loading. <Comments />
starts to hydrate instead<Sidebar />
.- Now, with the
<Comments />
load,<Sidebar />
turn back to hydrate. - 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.
It’s time to say goodbye!
In this article, I put together some information about how the new Suspense API works because we had a lot of behavioral differences, and now that it works in SSR, we can solve other problems.
I hope you enjoyed the content, and I will see you in the following article!
References
- https://betterprogramming.pub/how-suspense-works-in-react-18-c7617a50447f
- https://blog.bitsrc.io/understanding-the-suspense-api-in-react-18-bbea3f6f6df1
- https://medium.com/rootcodelabs/using-react-18s-suspense-to-improve-code-quality-of-web-loaders-6fbb1dd5ab2a
- https://github.com/reactjs/rfcs/blob/main/text/0213-suspense-in-react-18.md
- https://github.com/reactwg/react-18/discussions/37
- https://medium.com/jspoint/introduction-to-react-v18-suspense-and-render-as-you-fetch-approach-1b259551a4c0