Behavioral changes to Suspense in React 18

An overview of the

In the React 16.x release, we basically support Suspense. Suspense, however, wasn’t perfectly supported at the time, and a few things didn’t show up in our example, such as delayed variations (state transitions after parsing data), placeholder node flows (limiting nested and successive placeholders to reduce UI jitters), and SuspenseList (tweaking list or grid components, Such as sequential stream processing), etc. Suspense in React 16 and 17 is called Legacy Suspense for ease of differentiation.

All of our Suspense features rely on Concurrent React, which will be supported in React 18. This means Suspense will work slightly differently in React 18 than in previous versions. Technically, this is a breakthrough change, but as with automatic batch updates, the impact on existing code is expected to be relatively minor and the migration of applications will not be a significant burden.

This article focuses on the behavioral differences in Suspense — the parts that affect code compatibility for user components.

The term that

The feature itself is still called “Suspense”

The difference between Legacy Suspense and Concurrent Suspense is only important in the context of migration. Because we want most people to upgrade without any major obstacles, we won’t mention these terms outside of the migration scenario.

The sibling of the hover component is interrupted

Simple explanation

The basic user experience for Legacy Suspense and Concurrent Suspense is the same. In the following code example, the component ComponentThatSuspends will show the Loading component in its place during the request to process the data:

<Suspense fallback={<Loading />} ><ComponentThatSuspends />
  <Sibling />
</Suspense>
Copy the code

The main difference between the two is the influence of suspended Component on the rendering of its peers:

  • In Legacy Suspense, sibling components are immediately unmounted from the DOM, associated effects and life cycles are triggered, and the component is hidden. See the code example for details.
  • In Concurrent Suspense, sibling components are not unloaded from the DOM; the associated effects and life cycles are inComponentThatSuspendsTriggered when processing is complete. You can check it outCode sample.

Explain in detail

In previous React versions, there was a built-in impression that when a component starts rendering, it must finish rendering. For example, during the rendering of a class component, the Render method and component’s componentDidMount/Update lifecycle correspond 1:1. While most developers don’t really think about this process or use it subconsciously, they may inadvertently rely on it without realizing it.

It’s not hard to see how this is especially important for features like delaying rendering of child components until the data on which the component tree depends has been parsed. If a component is not ready to commit, what do we do with its siblings, some of which may already be rendering? (For example, if the third component in the list is hovering, the render method of the first two components will be called.)

When we first introduced Legacy Suspense, we found a clever way to keep the 1:1 render-commit correspondence going: we would skip hovering components, continue rendering sibling components, and update as much of the DOM as possible. This means that the DOM will have an inconsistent state, but we can avoid this because we’ll replace it with a Fallback UI anyway. Before allowing the browser to draw, we will display the Fallback UI and use display: Hidden to hide everything in Suspense boundaries.

Using this little trick, the rendering behavior of the sibling nodes is unaffected, but from the user’s point of view, they don’t see any inconsistencies: they just see a placeholder.

Legacy Suspense’s implementation, while a bit strange, is a good compromise that introduces the basics of Suspense in a backwards compatible way.

In Concurrent Suspense, what we do is break sibling components and prevent them from committing to the DOM tree. Content in Suspense boundaries, which include the component being hovered and its siblings, will not be submitted until the relevant data has been processed. The batch processing is then submitted to the entire tree in a consistent state. This fits well with our rendering model, both in terms of the complexity of the implementation and the functionality supported on top of it. From a developer’s point of view, this is arguably a more predictable behavior, since side effects should not be rendered on the page (this has been prevented).

Therefore, we need to make our code capable of supporting such interrupts. This has the same requirements as using startTransition. Typically, these implementations involve moving side effects and mutations from the render phase to the commit phase. You can use Strict Mode to catch these types of bugs early in the development process.

Ref outside Suspense boundaries

Another difference is actually caused by the render-commit problem: the time passed in by the parent ref.

const refPassedFromParent = useRef(null)
<Suspense fallback={<Loading />} ><ComponentThatSuspends />
  <button ref={refPassedFromParent} {. buttonProps} / >
</Suspense>
Copy the code

The Legacy of Suspense, at the beginning of the rendering refPassedFromParent. Current immediately to the DOM node, at this point ComponentThatSuspends haven’t complete the processing.

In Concurrent Suspense, unlocked the ComponentThatSuspends finished processing, Suspense border refPassedFromParent before. The current has been to null.

That is, any access to such refs in parent code requires attention to whether the ref is already pointing to the corresponding node.

We think the likelihood of this resulting in behavior differences is low, and in fact, the new behavior is more consistent with the rest of React’s rendering model. But it’s worth noting that it can affect existing code.


Ikoofe, the public account of wechat, “KooFE front-end team” releases front-end technology articles from time to time.