useDeferredValue: Deferring What You Don't Control
useDeferredValue defers the expensive downstream work from a value you receive — the alternative to useTransition when you don't control where state is set.
useTransition is the right tool most of the time. It lets you split a state update into an urgent part and a deferred part — and you control both.
But sometimes you receive a value from outside. A prop from a parent. A value from a library. Something from a context you don't own. You can't control how or when it's set. You can only react to the new value landing.
useDeferredValue is the tool for that case.
The Scenario
Imagine a component that receives query as a prop and runs an expensive filter:
function SearchResults({ query }: { query: string }) {
// Every time query changes, this runs — and it's slow
const results = useMemo(
() => fuzzySearch(allItems, query),
[query]
);
return <ResultsList results={results} />;
}The parent sets query on every keystroke. You can't move the state update — it's not yours. The parent might be a library component, a different team's code, or just a structural constraint you can't refactor around.
Every keystroke triggers fuzzySearch. The component lags. You're stuck — you can't use useTransition because you don't control the setter.
useDeferredValue gives you an escape:
function SearchResults({ query }: { query: string }) {
// deferredQuery lags slightly behind query
// React updates it in a low-priority lane
const deferredQuery = useDeferredValue(query);
const results = useMemo(
() => fuzzySearch(allItems, deferredQuery),
[deferredQuery] // ← uses deferred version, not the latest
);
return <ResultsList results={results} />;
}query updates immediately. deferredQuery follows when React has capacity — the same low-priority lane that startTransition uses. The useMemo is keyed on deferredQuery, so the expensive computation is deferred along with it.
Detecting the Pending State
useDeferredValue doesn't give you an isPending flag directly. But you can derive it: if the deferred value doesn't match the current value, a transition is in progress.
function SearchResults({ query }: { query: string }) {
const deferredQuery = useDeferredValue(query);
// true while deferredQuery hasn't caught up to query
const isPending = deferredQuery !== query;
const results = useMemo(
() => fuzzySearch(allItems, deferredQuery),
[deferredQuery]
);
return (
<div style={{ opacity: isPending ? 0.5 : 1 }}>
<ResultsList results={results} />
</div>
);
}When the user types, query updates immediately (the input shows the character). deferredQuery still holds the previous value — isPending is true. The results fade slightly. When React processes the deferred work, deferredQuery catches up — isPending becomes false, results return to full opacity.
useTransition vs useDeferredValue
Both hooks tap into the same lane priority system. The difference is where in the data flow you intercept:
useTransition | useDeferredValue | |
|---|---|---|
| You control the state setter | ✅ | ❌ |
| Wraps the update | startTransition(() => setState(...)) | Wraps the received value |
| Pending indicator | isPending from hook | value !== deferredValue |
| Best for | Your own state | Props, context, library values |
The practical heuristic: try useTransition first. If the state update is yours to control, wrapping it in startTransition is cleaner and gives you isPending for free.
Reach for useDeferredValue when you're on the receiving end of a value that will trigger expensive downstream work — and you can't touch where it comes from.
What the Deferred Value Actually Does
It's helpful to be concrete about what "deferring" means here.
When a new query prop arrives, React has two things to do:
- Re-render the parent (urgent — it's responding to a user event)
- Re-render
SearchResultswith the newdeferredQuery(deferred — it triggers expensive work)
React handles task 1 immediately. Task 2 is saved as low-priority work. If another user event arrives (another keystroke) before task 2 finishes, task 2 is abandoned and requeued with the latest value. This is the same interruptible behaviour from useTransition.
The net effect: the parent stays snappy. SearchResults shows the previous results while the new ones are being computed. When the computation finishes, the UI updates.
Avoiding a Common Mistake
The deferred value should drive the expensive work, not the original value:
// ❌ deferredQuery is computed but never used
const deferredQuery = useDeferredValue(query);
const results = useMemo(() => fuzzySearch(items, query), [query]); // still uses `query`
// ✅ deferredQuery drives the memo
const deferredQuery = useDeferredValue(query);
const results = useMemo(() => fuzzySearch(items, deferredQuery), [deferredQuery]);If the memo is keyed on the original query, it still invalidates on every keystroke. The deferred value is only useful if the expensive computation actually depends on it.
Practice what you just read.
Keep reading
Enjoyed this? Get more like it.
Deep dives on system design, React, web development, and personal finance — straight to your inbox. Free, always.