How React 18 Changes Suspense Behavior: What Developers Need to Know

This article explains the key differences between Legacy Suspense and Concurrent Suspense in React 18, covering how sibling components are handled, ref timing outside Suspense boundaries, and the impact on rendering and effects, helping developers adapt their code for the new model.

KooFE Frontend Team
KooFE Frontend Team
KooFE Frontend Team
How React 18 Changes Suspense Behavior: What Developers Need to Know

Overview

In React 16.x we had basic support for Suspense, but many features such as delayed state changes after data resolution, placeholder throttling, and SuspenseList were missing; we refer to the Suspense implementation in React 16 and 17 as Legacy Suspense. Full Suspense capabilities rely on Concurrent React, which is introduced in React 18, bringing subtle behavioral changes with minimal migration impact.

Terminology

This feature is still called "Suspense"

The distinction between Legacy Suspense and Concurrent Suspense matters only in migration contexts; we avoid mentioning these terms outside of upgrade scenarios.

Sibling components are interrupted

Simple explanation

Legacy Suspense and Concurrent Suspense share the same basic user experience. In the following code example, the component ComponentThatSuspends displays the Loading fallback while awaiting data:

<Suspense fallback=<Loading />>
  <ComponentThatSuspends />
  <Sibling />
</Suspense>

The difference lies in how the suspended component affects its sibling's rendering:

In Legacy Suspense, the sibling is immediately unmounted from the DOM, triggering its effects and lifecycle before being hidden.

In Concurrent Suspense, the sibling remains mounted; its effects run only when ComponentThatSuspends finishes processing.

Detailed explanation

Historically, developers assumed that once a component starts rendering it will finish, with a 1:1 correspondence between the render method and the componentDidMount/Update lifecycle. This assumption influences how Suspense delays rendering of child components until required data resolves. When a suspended component is not ready, we can skip its rendering, continue rendering siblings, and update the DOM as much as possible, then replace the inconsistent UI with a fallback using display:hidden to hide all content inside the Suspense boundary before the browser paints.

This technique ensures sibling rendering is unaffected while the user only sees the placeholder, providing a compatible way to introduce basic Suspense functionality.

In Concurrent Suspense, sibling components are not interrupted; instead, their rendering is paused until the suspended component resolves, after which the entire tree is committed in a consistent batch. This behavior aligns better with React's rendering model and offers more predictable effects handling.

Refs outside Suspense boundary

Another difference, also stemming from the render‑commit timing, concerns when parent refs are assigned.

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

In Legacy Suspense, refPassedFromParent.current points to the DOM node immediately at render start, even though ComponentThatSuspends has not yet completed.

In Concurrent Suspense, refPassedFromParent.current remains null until ComponentThatSuspends finishes and the Suspense boundary unlocks.

Therefore, code accessing such refs must check whether the ref has been assigned.

We consider the likelihood of this causing functional differences low; the new behavior aligns with React's other rendering models but may affect existing code.

frontendJavaScriptReActSuspenseConcurrent React
KooFE Frontend Team
Written by

KooFE Frontend Team

Follow the latest frontend updates

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.