useTransition (Async Support) vs useDeferredValue: The Completion of Concurrent Rendering

There is a fatal flaw in the search feature Chris built.
Every time the user types a search query, the app filters 10,000 items and repaints the list.
// โ The Laggy Search Bar
function SearchApp() {
const [query, setQuery] = useState('');
const handleChange = (e) => {
// Update input value (Urgent)
setQuery(e.target.value);
// List filtering logic executes, hogging the main thread -> Input stutters
};
return (
<div>
<input value={query} onChange={handleChange} />
<HeavyList query={query} />
</div>
);
}The screen stutters with every keystroke. Chris thought, "Should I use Debounce?" but since Debounce only reacts after the input stops, the search experience feels sluggish.
The Concurrency hooks introduced in React 18 solve this problem fundamentally. The core idea is "Process urgent updates immediately, and handle heavy updates later (Interruptible)."
Today, we will compare the two tools that perform this magic, useTransition and useDeferredValue, and even look at the async support features added in React 19.
1. useTransition: "Do the urgent things first"
useTransition is a hook that marks state updates as "Non-urgent."
In our situation:
Chris decided to wrap the list update with startTransition.
// โ
Applying useTransition
import { useState, useTransition } from 'react';
function SearchApp() {
const [query, setQuery] = useState('');
const [listQuery, setListQuery] = useState('');
const [isPending, startTransition] = useTransition();
const handleChange = (e) => {
const value = e.target.value;
// 1. Urgent Update: Input reacts immediately
setQuery(value);
// 2. Transition Update: List filtering is pushed to the back burner
startTransition(() => {
setListQuery(value);
});
};
return (
<div>
<input value={query} onChange={handleChange} />
{/* Loading state can be shown using isPending */}
{isPending && <span className="spinner">Filtering...</span>}
{/* The list watches listQuery */}
<HeavyList query={listQuery} />
</div>
);
}Now, when the user types quickly, React busily processes only setQuery and skips the work inside startTransition. Only when the typing stops does it render the list with the latest value. The input box never stutters.
Evolution in React 19: Async Transitions
Starting from React 19, you can put Async functions (Async/Await) inside startTransition!
This is also the underlying principle of useActionState that we learned earlier.
// React 19: Handling async tasks with Transition
const handleClick = () => {
startTransition(async () => {
// isPending automatically becomes true until the API call finishes.
await updateProfile(name);
});
};Now, you can control the UI during asynchronous operations without creating a separate isLoading state.
2. useDeferredValue: "I'll use this value later"
useDeferredValue shares the same goal as useTransition, but its usage differs.
While useTransition wraps the state updating function (Setter), useDeferredValue wraps the value itself.
It is useful when you want to defer values received via Props.
// โ
Applying useDeferredValue
import { useState, useDeferredValue } from 'react';
function SearchApp() {
const [query, setQuery] = useState('');
// query changes immediately, but deferredQuery changes when React has time.
const deferredQuery = useDeferredValue(query);
return (
<div>
{/* The input uses the original query for immediate reaction */}
<input value={query} onChange={e => setQuery(e.target.value)} />
{/* The list uses the deferred value */}
{/* It continues to show the old list until deferredQuery updates */}
<HeavyList query={deferredQuery} />
</div>
);
}The code is much simpler. It is extremely useful in situations where you cannot use startTransition (e.g., when using a value provided by a library as is).
3. Which One Should I Use? (Comparison)
Both hooks share the same goal of "Adjusting Rendering Priority."
| Feature | useTransition | useDeferredValue |
| Control Target | State Update Function (setState) | Value (props, variables) |
| Usage Location | Where state changes (Event Handler) | Where value is used (Child Component, Props) |
| Pros | Provides isPending state for loading UI | Concise code, good for useEffect dependency arrays |
| Analogy | "Process this task slowly" (Command) | "This value can be less fresh" (Data) |
Chris's Selection Guide:
4. How is it different from Debounce?
Therefore, for rendering optimization, Concurrency hooks are much more user-friendly (UX) than Debounce. (However, if the goal is to reduce API requests, Debounce is still necessary.)
Key Takeaways
We've now conquered rendering performance.
The final step in Built-in Hook Mastery is "Synchronization with the External World."
Are you aware of the Tearing issue that occurs when subscribing to window.resize events or creating external stores like Redux/Zustand via useEffect?
Continuing in: "useSyncExternalStore & useId: Essential Hooks for Library Developers and SSR."