useDeferredValue Patterns
The Value That Lags Behind
Think of useDeferredValue as the sibling of useTransition that you use when you don't own the state update. It takes a value and returns a "deferred" version that deliberately lags behind. During urgent renders, the deferred value keeps its old value. Once the urgent render completes, React schedules a background render with the new value.
function SearchResults({ query }) {
// deferredQuery lags behind query during heavy renders
const deferredQuery = useDeferredValue(query);
const isStale = query !== deferredQuery;
// Expensive filtering uses the deferred value
const results = filterProducts(deferredQuery);
return (
<div style={{ opacity: isStale ? 0.7 : 1 }}>
{results.map(r => <ProductCard key={r.id} product={r} />)}
</div>
);
}
When the user types "abc", each keystroke updates query immediately. But deferredQuery stays at the old value until React has time to process the expensive filter. The input is responsive. The list updates when the browser is free.
The Mental Model
Think of useDeferredValue as a news ticker that runs one story behind. The newsroom (parent component) receives breaking stories in real-time. The ticker (deferred value) shows the previous story while the new one is being typeset. The audience sees a seamless flow — the ticker is always one story behind, but it never pauses or flickers.
When the newsroom is calm (no heavy renders), the ticker catches up immediately. When stories come in rapid succession (fast typing), the ticker stays behind, showing the latest fully-processed story.
useDeferredValue vs useTransition
People always ask "which one should I use?" Both achieve similar results. The difference comes down to who owns the state update:
// useTransition: YOU control the state update
function SearchPage() {
const [query, setQuery] = useState('');
const [isPending, startTransition] = useTransition();
function handleChange(e) {
const value = e.target.value;
setQuery(value); // Can split: sync for input, transition for results
startTransition(() => {
setFilteredQuery(value); // You decide what's deferred
});
}
}
// useDeferredValue: SOMEONE ELSE controls the state update
function SearchResults({ query }) {
// You receive query as a prop — can't control how it was set
const deferredQuery = useDeferredValue(query);
// The deferral happens at the consumption point
}
Use useTransition when you own the state update (event handlers, actions).
Use useDeferredValue when you receive the value as a prop and can't control how it was set.
Practical Patterns
Pattern 1: Deferred List Rendering
function FilterableList({ items, filter }) {
const deferredFilter = useDeferredValue(filter);
const isStale = filter !== deferredFilter;
// Memoize the expensive rendering with the deferred value
const filteredItems = useMemo(
() => items.filter(item => item.name.includes(deferredFilter)),
[items, deferredFilter]
);
return (
<div style={{ opacity: isStale ? 0.6 : 1 }}>
{filteredItems.map(item => (
<ListItem key={item.id} item={item} />
))}
</div>
);
}
The useMemo combined with deferredFilter means the expensive filtering only runs when the deferred value catches up — not on every keystroke.
Pattern 2: Deferred Chart Updates
function RealTimeChart({ dataPoints }) {
// Data arrives at 60fps from a WebSocket
// Chart can't render 60 times per second
const deferredData = useDeferredValue(dataPoints);
return <Chart data={deferredData} />;
}
The chart renders when the browser is idle, not on every data tick.
Pattern 3: Deferred Suspense
function ProfilePage({ userId }) {
const deferredId = useDeferredValue(userId);
return (
<Suspense fallback={<ProfileSkeleton />}>
<ProfileContent userId={deferredId} />
</Suspense>
);
}
When userId changes, deferredId keeps the old value momentarily. React shows the old user's profile (stale but complete) instead of the Suspense fallback. Once the new user's data is ready, the deferred value catches up and the profile updates.
useDeferredValue only helps when the deferred value is used by an expensive child component or computation. If the component using the deferred value is trivially cheap to render, the deferral adds overhead (two renders) with no perceptible benefit.
// WASTEFUL: Badge is trivially cheap
function NotificationBadge({ count }) {
const deferredCount = useDeferredValue(count); // Pointless
return <span>{deferredCount}</span>; // Renders in microseconds anyway
}How useDeferredValue Works Internally
If you're curious what's happening under the hood, it's surprisingly straightforward. React treats useDeferredValue similarly to a self-triggered transition:
// Conceptual implementation (simplified)
function useDeferredValue(value) {
const [deferredValue, setDeferredValue] = useState(value);
useEffect(() => {
// Schedule an update at transition priority
startTransition(() => {
setDeferredValue(value);
});
}, [value]);
return deferredValue;
}
When value changes, React immediately returns the old deferredValue (fast render). Then it schedules a transition to update deferredValue to the new value (background render). If another value change arrives before the transition completes, the transition is interrupted and restarted.
The initial render behavior
On initial render (mount), useDeferredValue returns the same value as the input — no lag. The deferral only kicks in on subsequent updates. This means your component always shows correct data on first render, and only shows stale data during rapid updates.
// Initial render: deferredQuery === query (both are '')
// First keystroke: deferredQuery = '', query = 'a' (deferred lags)
const deferredQuery = useDeferredValue(query);Production Scenario: The Collaborative Spreadsheet
A spreadsheet app receives cell updates via WebSocket from other collaborators. Each update triggers re-rendering affected cells:
function Spreadsheet({ cells }) {
// cells change frequently from WebSocket updates
const deferredCells = useDeferredValue(cells);
const isStale = cells !== deferredCells;
return (
<div className={isStale ? 'updating' : ''}>
<table>
{deferredCells.map((row, i) => (
<tr key={i}>
{row.map((cell, j) => (
<Cell key={`${i}-${j}`} value={cell} />
))}
</tr>
))}
</table>
</div>
);
}
Without deferral: every WebSocket update triggers a full table re-render. 10 updates per second means the UI freezes.
With deferral: the table uses the deferred cells, rendering at whatever rate the browser can handle. The user's own edits (local state) are immediate. Remote updates render when idle.
Common Mistakes
| What developers do | What they should do |
|---|---|
| Using useDeferredValue when you own the state update useTransition gives you isPending and more control over which updates are deferred. useDeferredValue defers at the consumption point | Use useTransition when you control the setState call. useDeferredValue is for received values |
| Not combining useDeferredValue with useMemo Without useMemo, the expensive computation runs on both the fast render (stale value) and the background render (new value). useMemo skips the computation when the deferred value hasn't changed | Memoize expensive computations that use the deferred value |
| Using useDeferredValue on trivially cheap renders Deferral adds two render passes (fast with old value, then background with new). For cheap renders, this overhead exceeds any benefit | Only defer values that feed into expensive components or computations |
| Not showing a stale indicator when data is deferred Users see outdated data during deferral. Without a visual cue, they might act on stale information | Check value !== deferredValue and dim or mark stale content |
Challenge
Challenge: Choose the right tool
// For each scenario, decide: useTransition, useDeferredValue,
// or neither? Explain why.
// Scenario 1: A search input in your own component
function MySearch() {
const [query, setQuery] = useState('');
const results = expensiveFilter(data, query);
return (
<>
<input onChange={e => setQuery(e.target.value)} />
<ResultList items={results} />
</>
);
}
// Scenario 2: A chart component that receives data as props
function Chart({ data }) {
// data changes frequently from parent
return <SVGChart points=`{data}` />;
}
// Scenario 3: A button that toggles a boolean
function Toggle() {
const [on, setOn] = useState(false);
return <button onClick={() => setOn(!on)}>`{on ? 'ON' : 'OFF'}`</button>;
}
Show Answer
Scenario 1: useTransition
You own the setQuery call. Use startTransition to separate the input update (sync) from the filter update (deferred):
const [query, setQuery] = useState('');
const [filterQuery, setFilterQuery] = useState('');
const [isPending, startTransition] = useTransition();
function handleChange(e) {
setQuery(e.target.value); // Sync: input responsive
startTransition(() => setFilterQuery(e.target.value)); // Deferred: filter
}
const results = expensiveFilter(data, filterQuery);Scenario 2: useDeferredValue
You don't own the data source — it arrives as props. Defer at the consumption point:
function Chart({ data }) {
const deferredData = useDeferredValue(data);
return <SVGChart points={deferredData} />;
}Scenario 3: Neither
A boolean toggle is instantaneous. The render cost of showing/hiding one element is negligible. Adding transitions or deferral adds overhead with zero benefit.
Quiz
Key Rules
- 1useDeferredValue creates a copy that lags behind during heavy renders. The old value stays while the new renders in the background.
- 2Use useDeferredValue when you receive a value as a prop and can't control the state update. Use useTransition when you own the setState.
- 3Combine useDeferredValue with useMemo to skip expensive computations during the fast render pass.
- 4Check value !== deferredValue to detect stale content and show a visual indicator (dimming, loading bar).
- 5On initial mount, useDeferredValue returns the input value directly — no lag on first render.
- 6Only use with expensive renders. For cheap components, deferral adds overhead (two render passes) with no benefit.