React Concurrent Features: useTransition, useDeferredValue, and More
Master React's concurrent rendering features including useTransition, useDeferredValue, and strategies for keeping your UI responsive.
What you'll learn
- ✓What concurrent rendering means in React
- ✓Using useTransition to keep the UI responsive
- ✓Deferring expensive updates with useDeferredValue
- ✓Combining concurrent features with Suspense
- ✓Practical patterns and when to use each
Prerequisites
- •Solid React fundamentals
- •Familiarity with useState and useEffect
Concurrent rendering lets React work on multiple versions of the UI at the same time. Instead of blocking the main thread while rendering a large update, React can pause, work on something more urgent (like a keystroke), and then resume. The hooks useTransition and useDeferredValue give you control over which updates are urgent and which can wait.
The Problem: Blocking Updates
Imagine a search input that filters a list of 10,000 items. Without concurrent features, every keystroke triggers a full re-render of the list. The input feels sluggish because React cannot paint the new character until the entire list finishes rendering.
function SearchPage() {
const [query, setQuery] = useState('');
const filteredItems = items.filter(item =>
item.name.toLowerCase().includes(query.toLowerCase())
);
return (
<div>
<input
value={query}
onChange={e => setQuery(e.target.value)}
placeholder="Search..."
/>
<ItemList items={filteredItems} />
</div>
);
}
Every change to query is treated as equally urgent. The user’s typing and the list filtering compete for the same rendering cycle.
useTransition: Marking Updates as Non-Urgent
useTransition lets you mark a state update as a transition, meaning React can interrupt it if something more urgent comes in.
import { useState, useTransition } from 'react';
function SearchPage() {
const [query, setQuery] = useState('');
const [filteredItems, setFilteredItems] = useState(items);
const [isPending, startTransition] = useTransition();
const handleChange = (e) => {
const value = e.target.value;
setQuery(value); // Urgent: update the input immediately
startTransition(() => {
// Non-urgent: filter the list in the background
const filtered = items.filter(item =>
item.name.toLowerCase().includes(value.toLowerCase())
);
setFilteredItems(filtered);
});
};
return (
<div>
<input
value={query}
onChange={handleChange}
placeholder="Search..."
/>
{isPending && <p className="loading">Filtering...</p>}
<ItemList items={filteredItems} />
</div>
);
}
The setQuery update is urgent and renders immediately, keeping the input responsive. The setFilteredItems update is wrapped in startTransition, so React can defer it. If the user types another character before the list finishes rendering, React abandons the stale render and starts a new one.
The isPending flag is true while the transition is in progress, so you can show a subtle loading indicator without blocking the input.
useDeferredValue: Deferring a Derived Value
useDeferredValue is the declarative cousin of useTransition. Instead of wrapping a state setter, you wrap a value. React keeps showing the old value until it finishes rendering with the new one.
import { useState, useDeferredValue } from 'react';
function SearchPage() {
const [query, setQuery] = useState('');
const deferredQuery = useDeferredValue(query);
const filteredItems = items.filter(item =>
item.name.toLowerCase().includes(deferredQuery.toLowerCase())
);
const isStale = query !== deferredQuery;
return (
<div>
<input
value={query}
onChange={e => setQuery(e.target.value)}
placeholder="Search..."
/>
<div style={{ opacity: isStale ? 0.7 : 1 }}>
<ItemList items={filteredItems} />
</div>
</div>
);
}
The input updates instantly because query changes immediately. The list uses deferredQuery, which lags behind. React renders the list with the deferred value in the background. While the deferred render is in progress, the list fades slightly to indicate stale data.
When to Use Which
Use useTransition when you control the state update and want to split it into urgent and non-urgent parts. Use useDeferredValue when you receive a value from a parent (via props or context) and want to defer the expensive computation that depends on it.
| Scenario | Use |
|---|---|
| You own the state setter | useTransition |
| Value comes from a prop | useDeferredValue |
| Wrapping a navigation | useTransition |
| Deferring a child render | useDeferredValue |
useTransition with Navigation
Transitions work well for page navigations. Wrap the route change in startTransition so the old page stays visible while the new page loads, instead of showing a blank screen.
import { useTransition } from 'react';
import { useRouter } from 'next/navigation';
function NavLink({ href, children }) {
const router = useRouter();
const [isPending, startTransition] = useTransition();
const handleClick = (e) => {
e.preventDefault();
startTransition(() => {
router.push(href);
});
};
return (
<a
href={href}
onClick={handleClick}
style={{ opacity: isPending ? 0.6 : 1 }}
>
{children}
</a>
);
}
The link dims while the new page is loading, but the current page remains interactive.
Combining with Suspense
Concurrent features and Suspense are designed to work together. When a transition triggers a component to suspend (because it reads from a promise), React keeps showing the current UI instead of the Suspense fallback.
function App() {
const [tab, setTab] = useState('home');
const [isPending, startTransition] = useTransition();
const switchTab = (newTab) => {
startTransition(() => {
setTab(newTab);
});
};
return (
<div>
<nav>
<button onClick={() => switchTab('home')}>Home</button>
<button onClick={() => switchTab('posts')}>Posts</button>
<button onClick={() => switchTab('settings')}>Settings</button>
</nav>
<div style={{ opacity: isPending ? 0.7 : 1 }}>
<Suspense fallback={<Spinner />}>
{tab === 'home' && <Home />}
{tab === 'posts' && <Posts />}
{tab === 'settings' && <Settings />}
</Suspense>
</div>
</div>
);
}
Without startTransition, switching to Posts would immediately show the spinner. With the transition, React keeps showing the current tab while Posts loads in the background.
Avoiding Common Mistakes
Do not wrap urgent updates in transitions. Typing in an input, clicking a toggle, or opening a dropdown should always be urgent. Only defer the expensive downstream effects.
// Wrong: the input will feel laggy
startTransition(() => {
setQuery(e.target.value);
});
// Right: update input urgently, defer the filter
setQuery(e.target.value);
startTransition(() => {
setFilteredResults(filter(e.target.value));
});
Do not over-defer. If the computation is fast (filtering 50 items), using useDeferredValue adds complexity without benefit. These hooks help when the downstream render is expensive, typically hundreds or thousands of DOM nodes.
Check isPending placement. The pending indicator should be subtle: a faded list, a small spinner, or a progress bar. Avoid replacing content with a full-page loader, which defeats the purpose of keeping the old UI visible.
Performance Gains in Practice
Concurrent features do not make your code run faster. They make your UI feel faster by prioritizing what the user sees immediately. The total work is the same, but the input never freezes because React yields control back to the browser between rendering chunks. For CPU-intensive updates like filtering large datasets, rendering charts, or switching complex tabs, the improvement in perceived responsiveness is significant.
Related articles
- React React useTransition: Advanced Patterns for Responsive UIs
Go beyond basic useTransition usage with patterns for search, navigation, tab switching, and priority-based rendering in React concurrent mode.
- React React Performance: Memo, useMemo, and useCallback Guide
Optimize React performance with React.memo, useMemo, and useCallback. Learn when to use each, how to profile, and avoid common pitfalls.
- React React Suspense Orchestration: Advanced Loading Patterns
Master advanced React Suspense patterns including nested boundaries, SuspenseList coordination, transition integration, and skeleton architecture for complex UIs.
- React React Compiler and Automatic Memoization
Understand how the React Compiler works, what automatic memoization means, and how it eliminates the need for useMemo, useCallback, and React.memo.