Blogs/Technology

A Complete Guide to React Suspense and Concurrent Mode

Written byRiswana Begam A
Aug 4, 2026
13 Min Read
A Complete Guide to React Suspense and Concurrent Mode Hero
Too Long? Read This First

- Suspense coordinates loading UI: It displays the nearest fallback when a compatible child suspends during rendering.
- Suspense does not fetch data: Code splitting works directly, but data fetching must come from a Suspense-enabled framework, library, or cached Promise consumed through a supported API.
- “Concurrent Mode” is outdated terminology: Modern React provides concurrent rendering capabilities that are adopted gradually instead of enabling one global mode.
- Transitions mark updates as non-urgent: They allow urgent actions, such as typing or clicking, to interrupt less important rendering work.
- useDeferredValue Defers a value-driven part of the UI: It is useful when expensive results can temporarily lag behind an urgent input.
- Suspense boundaries are UX decisions: Their placement determines which content disappears, which fallback appears, and whether already revealed UI remains visible.
- Suspense needs error handling: A fallback handles waiting, while an Error Boundary handles rejected data or failed lazy imports.
- Use a framework for Suspense-enabled data fetching: React recommends framework-integrated data loading rather than inventing a custom Suspense data source.

As React applications grow, performance problems rarely come from rendering alone. They often appear when the interface must fetch data, download code, render expensive results, and respond to user input at the same time.

React Suspense and concurrent rendering address different parts of this problem. Suspense coordinates what users see while part of the component tree is not ready. Concurrent rendering allows React to prioritize urgent updates and interrupt less important rendering work.

These features are related, but they are not interchangeable. Suspense does not fetch data by itself, and concurrent rendering does not automatically make slow calculations fast.

This guide explains how both mechanisms work in modern React, how they interact, and which limitations developers must understand before using them in production.

First, What Happened to Concurrent Mode?

“Concurrent Mode” was the name used during React’s experimental development period. It described an all-or-nothing mode that would change how the entire application rendered.

React abandoned that rollout model before React 18. Instead, concurrent rendering is adopted gradually through individual features. React’s own announcement is explicit: there is no separate Concurrent Mode; there are concurrent features.

Modern applications interact with concurrent rendering through capabilities such as:

API or featurePrimary purpose
SuspenseCoordinates fallback UI for compatible asynchronous dependencies
startTransitionMarks state updates as non-urgent
useTransitionStarts a Transition and exposes its pending state
useDeferredValueAllows a non-critical value to lag behind an urgent update
StreamingSends server-rendered UI in sections as content becomes ready
React Server ComponentsMoves selected rendering and data access to the server
ActionsCoordinates async mutations, pending UI, errors, and optimistic updates
Suspense
Primary purpose
Coordinates fallback UI for compatible asynchronous dependencies
1 of 7

Concurrent rendering is mainly an implementation capability inside React. It allows rendering work to be paused, abandoned, or restarted before React commits the final result to the screen.

It does not mean that multiple JavaScript functions execute simultaneously on the main thread.

Understanding React Suspense

Suspense provides a declarative way to describe what React should display when part of the UI cannot finish rendering yet.

A Suspense boundary contains two important parts:

  • The content React should eventually display
  • A fallback React should display while that content is unavailable

When a compatible child suspends, React finds the closest Suspense boundary above it and temporarily renders that boundary’s fallback. Once the required code or data becomes available, React retries the suspended content.

This removes some loading coordination from individual components. Instead of every component deciding independently whether to show a spinner, the component tree defines meaningful loading boundaries.

Suspense With React.lazy

The most direct client-side use of Suspense is component code splitting with React.lazy.

import React, { Suspense } from 'react';
const MyComponent = React.lazy(() => import('./MyComponent'));

function App() {
  return (
    <Suspense fallback={<div>Loading...</div>}>
      <MyComponent />
    </Suspense>
  );
}

When MyComponent is first needed, React begins loading its module. Until that import resolves, the Suspense boundary renders Loading....

This allows a large application to avoid placing every component in its initial JavaScript bundle. Routes, dashboards, editors, modals, and other less frequently used sections can be downloaded when required.

The fallback should usually resemble the space and structure of the incoming content. Replacing a large page section with a small spinner can cause disruptive layout movement, whereas a stable skeleton can preserve the page’s visual structure.

Suspense With Data Fetching

Suspense can coordinate data loading, but it does not automatically detect an ordinary request started inside an Effect or event handler. React’s documentation explicitly notes that Suspense does not activate for data fetched that way.

The original example demonstrates the older resource-wrapper pattern:

import { Suspense } from 'react';
import { fetchData } from './api';

const resource = fetchData(); // wraps promise with a read() method

function DataComponent() {
  const data = resource.read();
  return <div>{data.message}</div>;
}

function App() {
  return (
    <Suspense fallback={<div>Loading data...</div>}>
      <DataComponent />
    </Suspense>
  );
}

In this pattern, resource.read() returns data when it is ready, throws a pending Promise while loading, or throws an error if the request fails. React interprets the pending Promise as suspension and displays the closest fallback.

This example is useful for understanding the mechanism, but it should not be treated as a recommended production data layer. React does not document a stable public contract for creating custom Suspense data sources independently.

For production data fetching, use one of the following:

  • A framework with documented Suspense and streaming support
  • A data library that explicitly documents its Suspense integration
  • A cached Promise passed to React’s supported use API
  • A React Server Component framework that manages request caching and streaming

React’s use API can read a cached Promise during rendering. While the Promise is pending, the component suspends; when it resolves, React retries the component. Rejected Promises propagate to an Error Boundary.

The important word is cached. Creating a new Promise during every render can repeatedly suspend the component and restart the work.

What Suspense Does and Does Not Do

Suspense doesSuspense does not
Display fallback UI for compatible suspended childrenAutomatically fetch data
Coordinate code-split component loadingDetect requests started in useEffect
Support streaming in compatible frameworksReplace error handling
Control which section reveals when readyMake network responses faster
Integrate with TransitionsCache requests automatically
Work with cached Promises and supported data sourcesProvide a universal client-side data-fetching API
Display fallback UI for compatible suspended children
Suspense does not
Automatically fetch data
1 of 6

This distinction prevents a common architectural mistake: wrapping an ordinary fetch request in <Suspense> does not make the request Suspense-aware.

Why Suspense Boundaries Matter

A Suspense boundary is not only a technical wrapper. It defines a visible loading transition.

If a boundary wraps the entire page, one slow child can replace the entire page with a fallback. If every small component has its own boundary, the page may become a collection of unrelated spinners and skeletons.

A useful boundary normally represents a section that can appear as one meaningful unit. Examples include a route, search-results panel, product recommendations, comments section, or account summary.

Nested boundaries can create progressive disclosure. The outer boundary reveals the page shell and primary content, while an inner boundary waits separately for slower secondary information.

The ideal structure follows the product’s loading experience rather than the component hierarchy alone.

Avoid Hiding Content That Is Already Visible

Suppose a tab panel is already displayed and a new render causes something inside it to suspend. React may replace that content with the nearest fallback.

Technically, this is correct. Visually, it can be unpleasant because useful content suddenly disappears and is replaced by a loading screen.

Transitions help React distinguish an urgent initial load from a non-urgent update to content that is already visible. During a Transition, React can keep the current interface on screen while preparing the next version in the background.

This interaction between Suspense and Transitions is one of the most important parts of the modern React loading model.

Suspense Needs an Error Boundary

A Suspense fallback handles waiting. It does not handle failure.

If a lazy import fails or a Suspense-enabled data request rejects, the error should be handled by an Error Boundary. Without one, the failure may propagate upward and remove a much larger part of the interface.

A production loading boundary should therefore answer three separate questions:

StateUI responsibility
PendingSuspense fallback
SuccessfulRequested component
FailedError Boundary fallback
Pending
UI responsibility
Suspense fallback
1 of 3

The error UI should ideally provide an appropriate recovery action, such as retrying the request, reloading the route, or returning to a stable screen.

Let’s Build Your React Native App Together!

We build powerful React Native apps that run smoothly on iOS and Android — fast, reliable, and ready to scale.

The Three Practical Benefits of Suspense

1. Loading UI Becomes Part of the Component Structure

Suspense places loading behaviour around the section that depends on the resource. This can be easier to understand than spreading multiple isLoading conditions across parent and child components.

That does not eliminate all loading state. Applications may still need progress indicators for mutations, background refreshes, optimistic updates, and actions triggered after the component has mounted.

Suspense is most valuable when the UI cannot render until compatible code or data becomes available.

2. Code Splitting Becomes User-Friendly

React.lazy prevents selected components from entering the initial bundle, while Suspense defines what appears during the download.

This is especially useful for large route-level sections or features users may never open. Splitting every small component, however, can add unnecessary requests and fallback transitions. Code-splitting boundaries should follow meaningful product sections.

3. Server-Rendered Content Can Stream Progressively

Frameworks can combine Suspense with server rendering to send completed parts of a page before every data dependency has finished.

For example, a navigation shell and page heading can appear first while a slower dashboard panel streams later. Users can begin reading or interacting with available content without waiting for the entire route.

Next.js App Router supports route- and component-level streaming through Suspense boundaries. Its loading file convention creates a route-level boundary automatically.

How Concurrent Rendering Works

Traditional synchronous rendering attempts to complete an update before React can attend to another render.

Concurrent rendering allows React to work on a new UI in the background without immediately committing every intermediate result. If a more urgent update arrives, React can pause or abandon the lower-priority render, process the urgent update, and later continue or restart the deferred work.

A common example is filtering a large result list while the user types.

The text field should update immediately because it reflects direct user input. Re-rendering an expensive results panel can be treated as less urgent. React can allow the input to stay responsive while preparing the updated list.

Concurrent rendering does not accelerate the filtering algorithm itself. If the calculation is fundamentally too expensive, it may still require memoization, virtualization, indexing, a Web Worker, or server-side processing.

Concurrency changes scheduling, not computational complexity.

Urgent and Non-Urgent Updates

The practical mental model is to separate updates by urgency.

Urgent updateNon-urgent update
Typing into an inputUpdating expensive search results
Pressing a buttonRendering the next tab’s content
Dragging an elementRefreshing a complex visualization
Opening an interactive controlApplying a large filter result
Showing direct input feedbackPreparing a route transition
Typing into an input
Non-urgent update
Updating expensive search results
1 of 5

Urgent updates should respond immediately. Non-urgent updates can be marked as Transitions or derived from a deferred value.

Using createRoot

The original React 18 migration path introduced createRoot:

import { createRoot } from 'react-dom/client';

const root = createRoot(document.getElementById('root'));
root.render(<App />);

Modern React applications and frameworks already use the current root API.

Calling createRoot is not the equivalent of enabling a special Concurrent Mode switch. It provides the modern root and enables React to use current rendering capabilities, while individual APIs determine which updates receive concurrent behaviour.

Using startTransition

startTransition marks state updates as non-urgent:

import { startTransition } from 'react';

startTransition(() => {
  setSearchQuery(input);
});

React can interrupt this update if a more urgent one arrives.

There is an important limitation in this exact example: Transition updates cannot control the text input itself. If searchQuery is the state bound directly to the input value, the input may not behave as intended.

A search interface commonly needs two conceptual values:

  • An urgent value that updates the input immediately
  • A non-urgent value used to render expensive results

Because the existing code is intentionally preserved, ensure setSearchQuery(input) represents the non-urgent results update rather than the state controlling the text field.

The standalone startTransition also does not expose pending status. When the UI needs to show whether a Transition is pending, use useTransition.

When to Use useTransition

useTransition returns a pending indicator and a function for starting a Transition.

It is useful when a component must:

  • Start a non-urgent state update
  • Keep the current UI interactive
  • Indicate that new content is being prepared
  • Avoid replacing already visible content with a large fallback

Navigation and tab changes are common examples. React specifically recommends that Suspense-enabled routers mark navigation updates as Transitions.

A Transition should not be used to delay every state update. Overusing it can make the interface feel disconnected because users do not see the result of their actions promptly.

When to Use useDeferredValue

useDeferredValue is useful when a component receives a value but does not control the state update that produced it.

The urgent interface can render with the latest value while an expensive child temporarily receives an older deferred version. React then attempts the deferred render in the background.

This is useful for search results, charts, previews, and large derived lists.

It differs from debouncing. Debouncing waits for a fixed interval before starting work. useDeferredValue has no fixed delay: React attempts the deferred render as soon as resources allow, and that render remains interruptible.

It also does not prevent network requests by itself. If a new value triggers a request, the request may still begin immediately even though React defers displaying its result.

How Suspense and Concurrent Rendering Work Together

The original flow remains a useful high-level illustration:

[User Action]
     |
     v
[Component Triggers Async Resource (e.g., fetch/image/lazy)]
     |
     v
[React detects resource delay]
     |
     v
[SUSPENSE kicks in] ---> Shows fallback (e.g., loader)
     |
     |--- (CONCURRENT MODE): Pauses work, processes user interactions
     |
[Resource is ready]
     |
     v
[React resumes rendering]
     |
     v
[Final Component is Displayed]

The terminology inside the original diagram should now be interpreted as concurrent rendering, not a separate Concurrent Mode.

A more precise description is:

  1. An update begins rendering.
  2. A compatible component suspends.
  3. React finds the nearest Suspense boundary.
  4. React may show its fallback or preserve existing content during a Transition.
  5. Urgent updates can interrupt lower-priority rendering.
  6. When the resource becomes available, React retries the suspended tree.
  7. React commits the completed UI.

Traditional Rendering vs Concurrent Rendering

AreaSynchronous renderingConcurrent rendering
Render workExpected to complete without interruptionCan be interrupted, discarded, or restarted
Update priorityUpdates receive less scheduling distinctionUrgent work can interrupt Transition work
Existing contentMay be replaced immediately during suspensionCan remain visible during a Transition
User inputExpensive rendering may delay feedbackUrgent input can commit before deferred work
Loading UIUsually coordinated manuallyCan be coordinated with Suspense
Performance effectDepends on work being performedImproves scheduling and perceived responsiveness
Render work
Synchronous rendering
Expected to complete without interruption
Concurrent rendering
Can be interrupted, discarded, or restarted
1 of 6

This table intentionally avoids universal timing claims. Values such as “2.2 seconds versus 1.4 seconds” are meaningful only when accompanied by a reproducible application, test environment, device, workload, and methodology.

Concurrent rendering does not guarantee a specific reduction in load time or input latency.

Using Suspense Directly

A compatible asynchronous component can be placed behind a Suspense boundary:

<Suspense fallback={<Loading />}>
  <MyLazyComponent />
</Suspense>

This boundary controls only descendants that suspend while React is rendering them.

If Loading also suspends, React moves upward to the next Suspense boundary. Fallback components should therefore remain lightweight and avoid introducing unnecessary asynchronous dependencies.

Practical Adoption Scenarios

Route-Level Code Splitting

Lazy-load a route or large feature and place a Suspense boundary around the relevant route outlet. This reduces the initial bundle without surrounding every small component with a loader.

Search and Filtering

Update the input urgently while deferring the expensive result render through a Transition or deferred value. Add list virtualization if rendering volume remains the bottleneck.

Tab Navigation

Mark the tab content update as a Transition so React can keep the current tab visible while preparing the next one. Use a subtle pending treatment instead of replacing the whole interface.

Let’s Build Your React Native App Together!

We build powerful React Native apps that run smoothly on iOS and Android — fast, reliable, and ready to scale.

Streaming Server-Rendered Pages

Use framework-supported Suspense boundaries to stream the page shell and fast content before slower sections are ready.

Data-Heavy Dashboards

Divide independent panels into meaningful boundaries. Avoid one boundary around the entire dashboard, which allows a single slow widget to hide everything else.

Common Mistakes

1. Treating Suspense as a Fetching Library

Suspense coordinates rendering around a compatible resource. It does not provide caching, request deduplication, invalidation, retries, mutations, or normalized data management.

2. Creating Promises During Render

An uncached Promise may be recreated on every attempt, repeatedly suspending the component. Suspense-compatible Promises must have stable caching or be supplied by a framework that manages their lifecycle.

3. Wrapping the Entire Application in One Boundary

One global boundary can replace the complete UI when a deeply nested component suspends. Place boundaries around meaningful loading sections instead.

4. Using Transitions for Controlled Inputs

Text input updates must remain urgent. Defer the expensive result update, not the state controlling what the user sees in the input.

5. Assuming Concurrent Rendering Makes Code Faster

React can schedule and interrupt rendering more effectively, but it cannot make an inefficient algorithm inexpensive. Profile the application and address the underlying bottleneck.

6. Forgetting Error Handling

Suspense handles pending resources. Use Error Boundaries to handle rejected Promises and failed lazy imports.

When You Should Not Use Suspense

Suspense may be unnecessary when an established loading-state implementation is simple, local, and already works well.

It is also a poor fit when the data source does not document Suspense support. Building a custom resource wrapper around arbitrary Promises creates a dependency on unstable integration details.

For background refreshes, form submissions, optimistic mutations, and event-driven requests, explicit pending states or React 19 Actions may express the user experience more accurately.

Suspense is most useful when a component genuinely cannot render until compatible code or data is ready.

React 19 and the Modern Async Model

React 19 extends the concurrent model with Actions. Transitions can include async work and coordinate pending states, errors, forms, and optimistic updates.

This does not make Suspense obsolete. The tools address different experiences:

  • Suspense coordinates rendering while code or data is unavailable.
  • Transitions classify updates as non-urgent.
  • Actions coordinate asynchronous mutations.
  • useDeferredValue Lets expensive derived UI temporarily lag behind.
  • Error Boundaries handle render-time failures.
  • Frameworks coordinate caching, routing, server rendering, and streaming.

Modern React performance work involves combining these tools according to the interaction instead of applying one universal loading pattern.

Frequently Asked Questions

Is Concurrent Mode still experimental?

There is no separate Concurrent Mode in modern React. Concurrent rendering features such as Transitions, Suspense for supported use cases, and useDeferredValue are stable. Individual newer APIs may have their own stability status.

Does Suspense fetch data automatically?

No. Suspense coordinates fallback UI when a compatible resource suspends. Use a framework, documented library integration, or a cached Promise consumed through a supported React API.

Can Suspense replace every isLoading state?

No. Explicit pending state is still useful for form submissions, mutations, background refreshes, progress reporting, and requests triggered by event handlers.

What is the difference between Suspense and a Transition?

Suspense defines what appears when content is not ready. A Transition marks an update as non-urgent so React can prioritize urgent interactions and potentially preserve already visible content.

What is the difference between startTransition and useTransition?

Both mark updates as Transitions. useTransition additionally provides an isPending value. The standalone startTransition is useful outside a component or when pending status is unnecessary.

Does concurrent rendering use multiple CPU threads?

Not for ordinary component rendering. React schedules and interrupts work on the JavaScript execution thread. Computationally expensive work may still require optimization or a Web Worker.

Can Suspense handle rejected requests?

The Suspense fallback handles the pending state. A rejected Promise must be handled by an Error Boundary or the framework’s error-handling mechanism.

Should I build my own Suspense data-fetching resource?

Generally, no. React states that the requirements for implementing Suspense-enabled data sources independently are unstable and undocumented. Prefer framework-managed or officially supported approaches.

Conclusion

React Suspense and concurrent rendering solve related but different UI problems.

Suspense allows a component tree to describe what users should see while compatible code or data is unavailable. Concurrent rendering gives React the flexibility to interrupt non-urgent rendering and prioritize direct user interactions.

Together, they make it possible to preserve responsive input, stream parts of a page progressively, keep existing content visible during navigation, and place loading experiences around meaningful sections of the interface.

The important architectural shift is not replacing every loading boolean with a Suspense boundary. It is identifying which work blocks rendering, which updates are urgent, which content can arrive later, and how failures should be handled.

Used with those distinctions in mind, Suspense, Transitions, deferred values, Error Boundaries, and framework-managed data loading provide a clearer foundation for building responsive modern React applications.

Author-Riswana Begam A
Riswana Begam A

I’m a tech returnee with a passion for coding, and I stay up-to-date with the latest industry trends. I have a total of 7 years of experience, with 3 years specifically in the tech field.

Share this article

Phone

Next for you

8 Best GraphQL Libraries for Node.js in 2025 Cover

Technology

Aug 4, 202613 min read

8 Best GraphQL Libraries for Node.js in 2025

8 Best GraphQL Libraries for Node.js in 2026 Too Long? Read This First - Choose Apollo Server when you need a mature ecosystem, GraphOS integration, plugins, or Apollo Federation. - Choose GraphQL Yoga for a modern, portable server with Fetch API compatibility and built-in support for subscriptions over Server-Sent Events. - Choose Mercurius when your application already uses Fastify and runtime efficiency is a major priority. - Use GraphQL.js when you need the official JavaScript implementati

9 React Native Animation Libraries and Tools Compared Cover

Technology

Aug 4, 202615 min read

9 React Native Animation Libraries and Tools Compared

Too Long? Read This First - Use React Native Reanimated for gesture-driven, interruptible, and performance-sensitive interface animations. - Use the built-in Animated API for simple fades, transforms, and timed sequences without another dependency. - Pair React Native Gesture Handler with Reanimated for swipes, dragging, pinching, rotation, and other touch-driven experiences. - Use Lottie React Native for non-interactive motion graphics supplied by designers. - Choose React Native Skia for cust

9 Critical Practices for Secure Web Application Development Cover

Technology

Aug 4, 202616 min read

9 Critical Practices for Secure Web Application Development

Too Long? Read This First - Define security requirements and model threats before implementation begins. - Treat authentication, account recovery, and MFA as one complete identity system. - Apply server-side authorization to every protected action and object. - Prevent injection with parameterized APIs, structured validation, safe output handling, and restricted outbound requests. - Protect sessions and tokens throughout their complete lifecycle. - Minimise sensitive data and manage encryption