Difficulty: Advanced
What are useTransition and useDeferredValue in React 18? When would you use each?
useTransition and useDeferredValue are two of the headline features of React 18's concurrent rendering, and they solve a problem that every developer has hit: slow UI responses when something expensive is happening.
Let me set the scene with a real example. You have a search input that filters a list of 10,000 items. Every time the user types a character, you update the search query AND re-render the filtered list. The problem is that filtering and rendering 10,000 items is slow -- maybe it takes 200 milliseconds. During those 200ms, the input feels laggy because React is busy rendering the list and cannot process the next keystroke. The user types "react" but the input shows "r...e...a...c...t" with visible delays. Terrible experience.
The key insight behind both of these hooks is the concept of urgent versus non-urgent updates. Typing in an input is urgent -- the user needs to see their keystrokes immediately or the UI feels broken. Filtering and rendering a big list is non-urgent -- the user can wait a beat for the results. Before React 18, there was no way to tell React about this priority difference. Every state update was treated with equal importance.
useTransition lets you mark a specific state update as non-urgent. You get back two things: isPending (a boolean that tells you the transition is still processing) and startTransition (a function you wrap around the non-urgent setState call). Here is how it works with the search example: you keep two pieces of state -- query (what the user typed, urgent) and searchQuery (what the list filters by, non-urgent). When the user types, you update query immediately (urgent), and inside startTransition, you update searchQuery. React processes the urgent update first so the input stays snappy, and then works on the transition in the background. If the user types again before the transition finishes, React abandons the in-progress transition and starts a new one. No wasted work.
The isPending flag is really useful for user experience. While the transition is processing, you can show a subtle loading indicator or dim the results list. This tells the user that new results are coming without blocking their interaction. It is way better than a full loading spinner that replaces the content.
Now, useDeferredValue solves a similar problem but from a different angle. The key difference is about ownership: useTransition is for when you own the state setter (you control the setState call), while useDeferredValue is for when you receive a value from somewhere else (like props or a parent component) and cannot wrap the original setState in startTransition.
With useDeferredValue, you pass it a value and it returns a deferred version. During an urgent update, the deferred value lags behind -- it still holds the old value while React prioritizes the urgent work. Once the urgent update is painted, React re-renders with the new deferred value. You can compare the original value with the deferred value to know if the results are stale (text !== deferredText) and dim the UI accordingly.
Think of useDeferredValue like a smarter version of debounce. Traditional debounce waits a fixed amount of time (say 300ms) regardless of how fast or slow the device is. useDeferredValue is React-driven -- on a fast device, the deferred update might happen almost immediately because React can handle it quickly. On a slow device, it might take longer, but the urgent updates stay responsive. It adapts to the actual performance of the device, which is way better than a hardcoded delay.
A crucial performance tip: for either of these hooks to actually help, the expensive component receiving the deferred or transition state should be wrapped in React.memo. Otherwise, the component re-renders with every keystroke regardless because React still passes the new props down. Memo lets React skip the re-render when the deferred value has not changed yet.
Here is when to use which: - You have a setState call and want to mark it as non-urgent: useTransition. - You receive a value from props or context and want to defer it: useDeferredValue. - You want to debounce an API call: neither. Use traditional debounce or throttle. These hooks are for expensive rendering work, not network requests.
One more thing worth mentioning: both of these require React 18's concurrent rendering, which is enabled by using createRoot. If you are still on the legacy ReactDOM.render, these hooks technically work but cannot actually interrupt renders, so you lose the main benefit.
import { useState, useTransition, memo } from 'react';
const HeavyList = memo(function HeavyList({ query }) {
// Simulates expensive filtering
const results = hugeList.filter(item =>
item.toLowerCase().includes(query.toLowerCase())
);
return (
<ul>
{results.map(item => <li key={item}>{item}</li>)}
</ul>
);
});
function SearchWithTransition() {
const [query, setQuery] = useState('');
const [searchQuery, setSearchQuery] = useState('');
const [isPending, startTransition] = useTransition();
function handleChange(e) {
const value = e.target.value;
setQuery(value); // Urgent: update input immediately
startTransition(() => {
setSearchQuery(value); // Non-urgent: defer list filtering
});
}
return (
<>
<input value={query} onChange={handleChange} />
{isPending && <span>Updating results...</span>}
<HeavyList query={searchQuery} />
</>
);
}
The input (urgent) updates immediately. The expensive list filter (non-urgent) is deferred. isPending shows a loading indicator during transition.
import { useDeferredValue, memo } from 'react';
const SlowList = memo(function SlowList({ text }) {
// Artificially slow rendering
const items = Array.from({ length: 10000 }, (_, i) => (
<li key={i}>{text} - item {i}</li>
));
return <ul>{items}</ul>;
});
// Parent controls the state
function App() {
const [text, setText] = useState('');
const deferredText = useDeferredValue(text);
const isStale = text !== deferredText;
return (
<>
<input
value={text}
onChange={e => setText(e.target.value)}
/>
<div style={{ opacity: isStale ? 0.5 : 1 }}>
<SlowList text={deferredText} />
</div>
</>
);
}
// useTransition: for state you own
// useDeferredValue: for values from props you don't own
// Both tell React: this update can be interrupted
useDeferredValue keeps showing the old value while computing the new one. The opacity dim provides visual feedback that results are stale.
useTransition, useDeferredValue, Concurrent React, isPending, Urgent Updates