patternjavascriptreactModerate
useDeferredValue delays rendering of slow children without blocking input
Viewed 0 times
React 18+
useDeferredValuedeferredconcurrent modeslow renderstale indicatormemo
Problem
A component receives a prop that triggers expensive child renders. You cannot use useTransition here because the value comes from a parent — you don't own the setter. The parent input stays responsive but the expensive child re-render blocks painting.
Solution
Use useDeferredValue to create a version of a value that lags behind during concurrent rendering:
import { useState, useDeferredValue, memo } from 'react';
function SearchPage({ query }) {
// Deferred — will lag behind query during concurrent rendering
const deferredQuery = useDeferredValue(query);
return (
<>
<input value={query} onChange={...} />
{/ SlowList gets the deferred value — renders at low priority /}
<SlowList query={deferredQuery} />
</>
);
}
// Memoize to make deferred value effective
const SlowList = memo(function SlowList({ query }) {
// This re-renders with the old query until the new one is ready
return <ul>{filterExpensive(allItems, query).map(item => <li key={item.id}>{item.name}</li>)}</ul>;
});
// Detect stale value to show visual indicator
function SlowPage({ query }) {
const deferredQuery = useDeferredValue(query);
const isStale = query !== deferredQuery;
return (
<div style={{ opacity: isStale ? 0.5 : 1 }}>
<SlowList query={deferredQuery} />
</div>
);
}
import { useState, useDeferredValue, memo } from 'react';
function SearchPage({ query }) {
// Deferred — will lag behind query during concurrent rendering
const deferredQuery = useDeferredValue(query);
return (
<>
<input value={query} onChange={...} />
{/ SlowList gets the deferred value — renders at low priority /}
<SlowList query={deferredQuery} />
</>
);
}
// Memoize to make deferred value effective
const SlowList = memo(function SlowList({ query }) {
// This re-renders with the old query until the new one is ready
return <ul>{filterExpensive(allItems, query).map(item => <li key={item.id}>{item.name}</li>)}</ul>;
});
// Detect stale value to show visual indicator
function SlowPage({ query }) {
const deferredQuery = useDeferredValue(query);
const isStale = query !== deferredQuery;
return (
<div style={{ opacity: isStale ? 0.5 : 1 }}>
<SlowList query={deferredQuery} />
</div>
);
}
Why
useDeferredValue is the prop-based equivalent of wrapping a state update in startTransition. React renders with the old deferred value first (skipping the expensive re-render) and schedules the update to the new value at low priority. The input remains responsive while the result catches up.
Gotchas
- Must memoize the child with React.memo — otherwise useDeferredValue has no effect (child re-renders anyway)
- During the deferred render, both old and new values coexist in the tree — only the final commit is shown
- useDeferredValue only helps in concurrent mode — if React isn't in concurrent mode it acts as a passthrough
- For server-rendered initial values, the deferred value starts equal to the initial value (no initial delay)
Code Snippets
useDeferredValue with stale indicator
const deferredQuery = useDeferredValue(query);
const isStale = deferredQuery !== query;
<div style={{ opacity: isStale ? 0.5 : 1, transition: 'opacity 0.2s' }}>
<MemoizedResults query={deferredQuery} />
</div>Context
When a prop change triggers a slow re-render in a child component you can't or don't want to modify with useTransition
Revisions (0)
No revisions yet.