How Suspense Works in React 18. Know the new Concurrent Suspense and… | by Jose Granja | Apr, 2022

The React Suspense feature was released as part of React 16 version. There, it had only one use case. It was meant to be used with its React.lazy API for code splitting. It would serve as a fallback when the element was not on yet downloaded and presented. It had one major drawback though. It could not be used on the server-side rendering engine.

It had other caveats however those were only temporary. It was well known that Suspense was a cornerstone of React’s concurrent engine mode. It was meant as much more than a code-splitting dedicated API.

With the Release of React 18, the Suspense feature has been further developed and enhanced. It fits many use cases and now it is compatible with SSR. It still can’t be used for data fetching though. This feature is still in experimental mode and might make it to a further release.

The React team provides a progressive update. Only by using the new ReactDOM.createRoot API we will unlock concurrency and all the fancy new Suspense features.

What is Suspense? It is a lower-level engine API that can be used to pause a component execution. How is that done? In a nutshell, it all boils down to a component throwing a Promise that is intercepted by the engine. It will defer the execution of that component tree until that Promise is resolved or rejected.

We can see a bit of the API if we look at React’s source code.

The fallback attribute of Suspense is where we specify the component’s loading behavior. What happens if there are multiple Suspense wrappers? The fallback from the closest parent in the JSX tree will be used.

Let’s see an example:

The stable version of suspense found in 16 and 17 was the synchronous implementation of the concurrent idea. It was known as Legacy Suspense.

In the Legacy Suspense, the component tree would be just be hidden from the UI. It won’t be discarded as it should. That would be done by adding the display: none style to the parent DOM element. This implementation led to inconsistencies in the firing of lifecycle events. Those were fired prior to the ComponentThatSuspends getting resolved. It led to quite a few bugs and undesired behaviors.

In the Concurrent Suspense version, the component will be discarded when suspended. The uncompleted render trees don’t get committed. Only when the component is ready it will be placed in the DOM. Its layout effects will then be fired. The execution order is therefore much more intuitive. The components are now purely asynchronous.

Legacy Suspense

  • The element tree is immediately mounted in the DOM
  • Effects/Lifecycle are fired
  • The tree is visually hidden when suspense is triggered
  • It is made visible only after the ComponentThatSuspends is resolved

Concurrent Suspense

  • Element is not mounted until the ComponentThatSuspends is resolved
  • Effects/Lifecycle are fired

Why did not the React team directly implement the Concurrent Supense approach? It was because of the legacy Class Components lifecycle. There was no proper way for theConcurrent Suspense engine to deal with events like componentWillMount. That is the reason behind the React team prefixing all those with UNSAFE_.

We have just scratched the surface previously. How would those layout events be executed in Concurrent Suspense? Some components can be removed and re-added later to the DOM.

Those will now run now on the hide and show events. When React needs to hide the suspended nodes it will run their cleanup function. When needing to show the suspended elements it will retrigger their layout effects.

Contrary to what is happening with Legacy suspense those layout effects may run several times. It is not true anymore that layout effects with [] dependencies will just run once. It is better not to think of those in terms of lifecycle events but as units of behavior for the different React features.

If we look at the below example:

We might get several log on mount and log on cleanup log statements. It depends if the component gets suspended and re-added multiple times.

The Legacy Suspense would throw an error when used in SSR. It was inconvenient.

A new HTML concurrent server-rendered has been added. Instead of producing a string it does output a stream.

That stream can be used to push early the initial HTML. It will include the Suspense fallback placeholders. When the content is ready it will emit an HTML fragment with a script tag to inject the components in the right place. The React library will be able to hydrate parts of the application while the stream is not completed.

This stream feature will come in really handy when Concurrent Suspense supports data fetching.

Let’s see the API that makes it all possible:

Let’s see a representation of how this looks in the Browser

Representation of SSR streaming HTML with Suspense from the React team found here.

The green area placeholders are elements that have been hydrated. The spinner is a component that is suspended on the server and yet not streamed.

For more information, you can see the fulls details here.

With React 18 we get another suspense engine feature: <SuspenseList />. We can use this component to wrap multiple <Suspense /> instances. With this feature, we can coordinate and orchestrate how those are revealed to the user. This helps mitigate the unpredictability of the network.

It takes two props:

  • revealOrder: it defines the order in which they should be revealed. The options are forwards, backwards, together.
  • tail: it lets us collapse or hide all the suspense fallbacks. The values ​​are collapse or hidden. By default, it will display all the suspense fallbacks.

Let’s see a usage example:

Notice how in the above example the order of appearance to be forwards. This will show the components appearing in a sequential order.

The React 18 release comes with some new APIs that can be used to further fine-tune the Suspense experience.

We might want to keep loaded components around while the new ones are being fetched. The user would keep on seeing relevant info while the download/parsing of the component is happening behind the scene. For that, we can use the transition API.

Let’s look at a concrete example. Let’s imagine we have a set of tabs that the user can navigate around. When switching tabs, it is more relevant to display the old tab content instead of showing the Suspense fallback for the new content. The page should stay interactive. If the user was to click on another tab the engine will be discarding the current suspense task and load the new one.

Let’s see the example on code. Let’s see the pre-React 18 version:

Let’s use the new transition API. The useTransition hook API provides a progress indicator [0] and a method to start the less critical transition [1].

The page will start interactive at all times while the processing happens asynchronously.

It is important to give feedback to the user that something is happening. By using the isPending boolean we can dim the content. The action pressing a tab has a direct reaction: content is dimmed.

How is this working? The React engine is keeping the previous version tree of the UI while it waits for the new one to be completed. It is like React is working concurrently in another branch. The will be two versions of the same JSX tree.

With this transition API, we are now more in control of how we want our application to behave. We have more control over its execution. It will all boil down to our specific UX scenario.

We have seen all the cool features from the Concurrent Suspense API. It is a massive improvement in performance. It also fixes a few bugs related to the Legacy Suspense implementation. Just the Suspense on SSR alone is worth the upgrade to React 18.

Although this release is super exciting we just can’t wait for the Suspense data fetching to be ready. It will be a massive game-changer unlocking many patterns. We have to be patient. The difficulty lies in that there are many moving pieces that need to be synchronized.

It has taken some time to put this release together. However their implementation of Suspense was worth the wait.

Thanks for reading.

Leave a Comment