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>
);
}