useDeferredValue: A Lagging Copy
What you'll learn
- What
useDeferredValuereturns, and why "lagging" is the right mental image - The two renders hidden inside the hook
- The canonical use case: keeping a slow list off the input's critical path
useDeferredValuevsstartTransitionvs debounce, when each one applies- The staleness UX pattern, and the number-one misuse
Last chapter you marked an update as non-urgent with startTransition. But sometimes you don't control the update, the value arrives from props, a parent, a router, a library. There's no setState call site to wrap. useDeferredValue flips the API around: instead of deferring the setter, you defer the value where you consume it.
What the hook returns
Jargon: "deferred value". A copy of a value that intentionally lags behind the real one. React keeps rendering with the old copy until a background render with the new copy is ready to commit.
const deferredText = useDeferredValue(text);
The contract, precisely:
- When
textchanges, React first renders urgently with the olddeferredText, the hook still returns the previous value. - Then React starts a background transition render where the hook returns the new value.
- Until that background render commits,
deferredTextkeeps returning the old value. - If
textchanges again mid-render, the background draft is discarded and restarted with the newest value. - When the background render finally commits,
deferredText === textagain, the lag has caught up.
It's last chapter's two-render mechanism, packaged as a hook: you get the urgent render automatically, and the transition render carries the new value.
The canonical example: search input + slow list
import { useState, useDeferredValue, memo } from 'react';
function burnCpu(milliseconds) {
const start = performance.now();
while (performance.now() - start < milliseconds) {}
}
const SlowResults = memo(function SlowResults({ query }) {
const items = [];
for (let i = 0; i < 200; i++) {
burnCpu(1); // ~200ms: pretend each result row is expensive
items.push(
<li key={i}>
Result {i} for “{query || '…'}”
</li>
);
}
return <ul>{items}</ul>;
});
export default function SearchPage() {
const [text, setText] = useState('');
const deferredText = useDeferredValue(text);
const isStale = text !== deferredText;
return (
<div>
<input
value={text}
onChange={(e) => setText(e.target.value)}
placeholder="Search…"
/>
<div style={{ opacity: isStale ? 0.5 : 1 }}>
<SlowResults query={deferredText} />
</div>
</div>
);
}
What happens:
- You type r.
setText('r')is urgent, it's your keystroke echo. - Urgent render: the input shows
rinstantly.useDeferredValuestill returns the old'', soSlowResultsgets the samequeryprop as before, and beingmemo'd, it's skipped entirely. This render is tiny; it commits within a frame. - Background render starts with
query = 'r': ~200ms of rows, time-sliced and interruptible. - You type e while it's running. The
rdraft is discarded; a new background render starts withquery = 're'. - You pause. The newest background render finishes and commits: the list shows results for
re,isStaleflips tofalse, and the dimming clears.
Every keystroke costs one tiny urgent render. The expensive list updates at whatever rate the machine can afford, and only ever renders the latest query, never every intermediate one.
Two details worth burning in:
memois load-bearing. Without it, every urgent render would re-runSlowResultswith the old query, 200ms back on the critical path, and the hook buys you nothing. Deferred value + memoized consumer is the pair.- The input uses
text, neverdeferredText. Hold that thought; it's the misuse section.
vs startTransition: same engine, different steering wheel
Under the hood there is one mechanism, transition-lane renders with time slicing and discarding. The two APIs differ only in where you point it:
- You call the
setStateyourself → wrap the call site:startTransition(() => setTab(x)). - The value comes from outside, props from a parent, state owned by a library, a URL param from the router → there's no call site to wrap, so defer the value where you consume it:
const deferred = useDeferredValue(value).
Rule of thumb: transitions defer updates you own; useDeferredValue defers values you're given.
vs debouncing: waiting vs adapting
Jargon: "debounce". A classic hand-rolled technique: don't act on a stream of events until the stream goes quiet for N milliseconds. Typing pauses for 300ms → now do the search.
Debouncing works, but look at what it actually does:
- It waits for silence. While the user types, the list never updates, then updates all at once, arbitrarily late.
- The delay is fixed and blind. 300ms is wasted time on a fast machine, and on a slow machine the heavy render still freezes the page when it finally fires.
A deferred value behaves differently on both axes:
- It keeps updating during typing, at whatever rate the device can afford. Each background render carries the newest value, and drafts for stale intermediate values are thrown away.
- It's adaptive. On a fast laptop the background render finishes nearly instantly, so
deferredTextbarely lags. On a cheap phone it lags more, but the input never freezes, at any hardware tier.
Debounce says "guess how long the user pauses". Deferred value says "update as fast as this machine can, without blocking anyone".
The staleness UX pattern
A lagging list is honest only if the user can tell it's catching up. The hook hands you the signal for free:
const isStale = text !== deferredText;
While a background render is pending, the real value and the deferred value differ, so dim the stale content, grey it, show a subtle spinner. When the commit lands, they match again and the UI clears. One comparison gives you a designed, intentional "this is updating" state instead of a mysterious lag.
The number-one misuse
Deferring the input's own value:
import { useState, useDeferredValue } from 'react';
export default function BadSearch() {
const [text, setText] = useState('');
const deferredText = useDeferredValue(text);
return (
<input
value={deferredText} // DON'T: the input itself now lags behind your fingers
onChange={(e) => setText(e.target.value)}
/>
);
}
What happens: typing feels drunk, letters appear a beat after you press them. You deferred the one thing that must never lag: the direct feedback of the interaction (last chapter's urgent pile). The input is the urgent UI; defer the expensive consumer of the value, the list, the chart, the preview, never the source.
Diagram
Rendered diagram (PNG hi-res):
Rendered diagram (PNG hi-res):
Common misconceptions
- Misconception:
deferredTextupdates on a timer. Reality: no timers, it updates when the background render commits, which depends on how busy the device is. - Misconception:
useDeferredValuereplacesmemo. Reality: they're partners. The hook schedules the background render;memokeeps the urgent render cheap. You usually need both. - Misconception: It's just debounce with React branding. Reality: debounce waits for a fixed silence and then blocks; deferred value keeps streaming updates at the device's own pace, adaptively.
- Misconception: Stale-looking UI is a bug. Reality: it's the designed trade, and
text !== deferredTexthands you the exact signal to present it honestly. - Misconception: More deferring is always better. Reality: if the consumer is cheap, deferring adds renders for nothing. Use it where the consumer is provably expensive.
Why it works this way
- Value-based API = works where setters don't. When the update is owned by a parent, a router, or a library, the consumption point is the only place you control.
- One mechanism, two doors.
useDeferredValuereuses the transition machinery, nothing new under the hood, nothing new to debug. - Adaptive beats fixed. A scheduler that responds to the actual machine beats any hand-picked delay, on both fast and slow hardware.
- Staleness is exposed, not hidden.
text !== deferredTextturns an implementation detail into a design tool.
Try it yourself
- Run
SearchPageand type quickly. Expected: input instant, list dims and catches up when you pause. Now passtextdirectly toSlowResultsinstead ofdeferredText. Expected: typing freezes on every keystroke. - Keep the hook but remove
memofromSlowResults. Expected: the freeze returns, the urgent render re-runs the whole list with the old query. Lesson: hook + memo, together. - Log
textanddeferredTexton every render while typing. Expected:deferredTextvisibly trails during fast typing and snaps equal when you stop. - Change the list to 20 rows, then to 2,000. Expected: with 20, the lag is barely visible; with 2,000, the list lags a lot, but the input never freezes. That contrast is adaptivity.
Recap
useDeferredValue(value)returns a lagging copy: old value during the urgent render, new value once the background render commits.- The mechanism is last chapter's two renders: urgent render with the old value, transition render with the new one.
- Canonical use: instant input + slow memoized list. The hook and
memoare a package deal. - Use
startTransitionwhen you own thesetState; useuseDeferredValuewhen the value comes from outside. - Unlike debounce, deferred values keep updating during typing and adapt to the device's speed.
- Never defer the input's own value, defer the expensive consumer, and dim stale content with
text !== deferredText.