How to Implement Concurrent Mode in React

This article provides a practical guide on how to implement and leverage Concurrent Mode features in React. You will learn how to enable concurrent rendering by upgrading your application’s entry point to React 18’s root API, and how to use concurrent hooks like useTransition and useDeferredValue to keep your user interface responsive during heavy state updates.

In React 18 and newer versions, “Concurrent Mode” is no longer a distinct, all-or-nothing mode. Instead, it is enabled by default through concurrent rendering when you adopt the modern mounting API.

Step 1: Enable Concurrent Rendering with createRoot

To unlock concurrent features, you must migrate from the legacy ReactDOM.render API to the new createRoot API. This is the foundational step that enables concurrent rendering under the hood.

Replace your legacy entry point code (usually in index.js or main.js):

// Legacy approach (React 17 and earlier)
import ReactDOM from 'react-dom';
import App from './App';

ReactDOM.render(<App />, document.getElementById('root'));

With the modern root API:

// Modern approach (React 18+)
import ReactDOM from 'react-dom/client';
import App from './App';

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

Once you make this change, React automatically switches to concurrent rendering, allowing you to use concurrent features.

Step 2: Implement useTransition for Non-Urgent Updates

The useTransition hook allows you to mark state updates as non-urgent transitions. This prevents heavy state updates (like filtering a massive list) from blocking urgent user interactions (like typing in an input field).

Here is how to implement it:

import { useState, useTransition } from 'react';

function SearchList() {
  const [isPending, startTransition] = useTransition();
  const [filterTerm, setFilterTerm] = useState('');
  const [resolvedTerm, setResolvedTerm] = useState('');

  const handleChange = (e) => {
    // Urgent update: Update the input field immediately
    setFilterTerm(e.target.value);

    // Non-urgent update: Defer the heavy list filtering
    startTransition(() => {
      setResolvedTerm(e.target.value);
    });
  };

  return (
    <div>
      <input type="text" value={filterTerm} onChange={handleChange} />
      {isPending ? <p>Loading results...</p> : <List term={resolvedTerm} />}
    </div>
  );
}

Step 3: Implement useDeferredValue for Deferred Rendering

When you do not have direct control over the state setter function (for example, if the value is passed down as a prop from a parent component or third-party library), use the useDeferredValue hook. This hook returns a deferred version of a value that lags behind urgent updates to prevent UI lag.

import { useDeferredValue } from 'react';

function MyComponent({ rawValue }) {
  // React will defer updating this value if there are urgent renders to process
  const deferredValue = useDeferredValue(rawValue);

  return (
    <div>
      <HeavyComponent value={deferredValue} />
    </div>
  );
}

Step 4: Integrate with Suspense for Smooth Loading States

Concurrent rendering works hand-in-hand with <Suspense>. You can wrap components that perform asynchronous operations in a Suspense boundary to show a fallback UI while data is loading, without blocking the rest of the application.

import { Suspense } from 'react';

function App() {
  return (
    <div>
      <h1>My Application</h1>
      <Suspense fallback={<div>Loading profile...</div>}>
        <UserProfile />
      </Suspense>
    </div>
  );
}