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.

March 22, 20264 min read3 / 5

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:

TSX
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:

TSX
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.

TSX
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:

useTransitionuseDeferredValue
You control the state setter
Wraps the updatestartTransition(() => setState(...))Wraps the received value
Pending indicatorisPending from hookvalue !== deferredValue
Best forYour own stateProps, 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:

  1. Re-render the parent (urgent — it's responding to a user event)
  2. Re-render SearchResults with the new deferredQuery (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:

TSX
// ❌ 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.

Defer a Received Value with useDeferredValue
1 exercise

Enjoyed this? Get more like it.

Deep dives on system design, React, web development, and personal finance — straight to your inbox. Free, always.