The release of React 18 Alpha has brought a lot of excitement amongst the React community. It is more focused on user experience, architecture changes, and adaptation of concurrent features.
One of the features in React 18 everyone is talking about is Suspense. It is a vast topic to discuss as many changes have been introduced related to Suspense. In this blog, we will look into the improvements coming up in React 18 related to Suspense.
What is Suspense?
As the name implies, it suspends something until it is ready.
React 16.6 added a <Suspense>
component that lets us ‘wait’ for some code to load
and specify a loader while we are waiting for the code to finish loading.
The React team added basic support for Suspense in React 16, which missed a lot of planned features. A full suite of Suspense functionality that depends on Concurrent React was added in React 18.
In the context of migration, the version of Suspense that exists in 16 and 17 is referred to as ‘Legacy Suspense’ while that in React 18 is referred to as ‘Concurrent Suspense’. The feature itself is still called just “Suspense”.
Let’s look into the behavioral changes in Suspense before and after React 18.
Before
Consider we are building our blog in React.
We wrap <BlogPost>
and <Share>
components by Suspense
similar to an example mentioned in the
discussion.
React will display the ‘Loading blog…’
until the blog post is fetched.
On running the application, we see the below output in the browser console -
We see that the Share(sibling) component immediately mounts to the DOM and its effect is fired without waiting for the blog post to be fetched.
React skips over the BlogPost component and proceeds to render the Share component,
committing whatever possible.
But we do not see the Share component immediately instead,
we see ‘Loading blog..’ text in the UI.
It is because before the browser is allowed to paint,
React shows the fallback UI and hides everything inside Suspense with display: hidden
till the blog is fetched.
Although the DOM is in an inconsistent state,
the user does not see any inconsistency in UI due to this trick.
But this behavior causes some issues when the child component needs to read from the DOM layout to correctly display its elements.
After
Running the previous example in React 18 with createRoot
gives the below output in the console.
With the changes in React 18 with createRoot React interrupts the Share component(sibling) and prevent it from committing, thereby delaying effects from firing until the blog post is fetched.
The whole tree is committed simultaneously in a single, consistent batch after the data is fetched.
The Impact of the change on the timings of refs in Suspense boundary
To understand the impact on refs,
let’s modify the previously discussed example with a button
having ref
.
In Legacy Suspense,
buttonRef.current
will point to a DOM node immediately on the initial render irrespective of the data fetched.
Whereas,
in Concurrent Suspense,
buttonRef.current
will be null
until BlogPost resolves and the Suspense boundary is unblocked.
Any code in an ‘App’ component that accesses buttonRef
will observe a change in the timing of when the ref resolves.
To summarize, React’s new behavior in Suspense is now consistent with the rest of React’s rendering model.
To know more about the Suspense behavioral changes, check out the WG discussion.