useDeferredValue
useDeferredValue lets you keep the UI responsive while an expensive part of the screen catches up to a fast-changing value. You hand it a value, and it returns a possibly-stale copy that lags behind during heavy renders. The fast inputs (like a text field) stay snappy, while the slow children re-render in the background using React’s concurrent renderer. It is one of the simplest tools for fixing the classic “typing feels laggy because a big list re-renders on every keystroke” problem.
How it works
You call useDeferredValue(value) and use the returned deferredValue to drive the slow part of your tree. On the first render the deferred value equals the value you passed. After that, when the input value changes, React renders the component twice: once immediately with the old deferred value (so urgent UI updates right away), and then again in the background with the new deferred value. If the value changes again before the background render finishes, React throws away the in-progress work and restarts — so you never block on stale output.
import { useState, useDeferredValue } from "react";
import SearchResults from "./SearchResults";
export default function SearchPage() {
const [query, setQuery] = useState("");
const deferredQuery = useDeferredValue(query);
return (
<div>
<input
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Search products…"
/>
{/* The input uses the fresh `query` and stays responsive.
The expensive list reads `deferredQuery` and lags behind. */}
<SearchResults query={deferredQuery} />
</div>
);
}
The crucial detail: the <input> binds to the fresh query, so typing always feels instant. <SearchResults> reads deferredQuery, so its expensive render is deprioritized and can be interrupted by the next keystroke.
A search-results example
To make the deferral meaningful, the slow child needs to actually be slow. Memoizing it ensures it only re-renders when its props change, which is what allows the deferred value to gate its work.
import { memo } from "react";
const SearchResults = memo(function SearchResults({ query }) {
// Pretend filtering this list is expensive.
const items = filterProducts(query);
return (
<ul>
{items.map((item) => (
<li key={item.id}>{item.name}</li>
))}
</ul>
);
});
function filterProducts(query) {
const q = query.toLowerCase();
return PRODUCTS.filter((p) => p.name.toLowerCase().includes(q));
}
export default SearchResults;
Without memo, the child re-renders on every parent render regardless of whether query changed, and the deferral buys you nothing.
Showing a stale indicator
Because the deferred value lags, you can detect the “in between” moment by comparing it to the live value and dim the stale content.
import { useState, useDeferredValue } from "react";
export default function SearchPage() {
const [query, setQuery] = useState("");
const deferredQuery = useDeferredValue(query);
const isStale = query !== deferredQuery;
return (
<div>
<input value={query} onChange={(e) => setQuery(e.target.value)} />
<div style={{ opacity: isStale ? 0.5 : 1, transition: "opacity 0.2s" }}>
<SearchResults query={deferredQuery} />
</div>
</div>
);
}
Tip: React 19 also added an
initialValueargument —useDeferredValue(value, initialValue)— which lets the very first render use a lighter placeholder before deferring to the real value.
useDeferredValue vs useTransition
Both defer work using the concurrent renderer, but they wrap different things. useTransition defers a state update you control, so it suits cases where you own the setState call. useDeferredValue defers a value you receive, so it suits cases where the value comes from props or from state you would rather not restructure.
| Aspect | useDeferredValue | useTransition |
|---|---|---|
| You wrap | A value | A state update (setState inside startTransition) |
| Need access to the setter? | No | Yes |
| Pending flag | Derive via value !== deferredValue | Built-in isPending boolean |
| Best for | Props you don’t own, third-party values | Updates you trigger yourself (tab switches, filters) |
| Returns | The deferred value | [isPending, startTransition] |
If you own the update, useTransition is usually clearer. If you only have a value handed to you — for example from a context or a parent — reach for useDeferredValue.
When it does not help
useDeferredValue does not make rendering faster; it only changes when the slow render happens and lets it be interrupted. If your child is slow because of a synchronous, uninterruptible computation that always blocks the main thread, you still need to optimize that work (memoization, virtualization, or moving it to a Web Worker). The hook also has no effect if the child is not memoized, since it would re-render anyway.
Best practices
- Bind fast inputs to the live value and only feed the deferred value to expensive children.
- Wrap the slow child in
React.memoso the deferred value can actually gate its re-renders. - Use
value !== deferredValueto show a subtle stale indicator instead of a blocking spinner. - Prefer
useTransitionwhen you own the state update; reach foruseDeferredValuefor values you only receive. - Avoid creating a new object or array as the deferred value’s source each render — it defeats memoization.
- Pair with virtualization for very large lists; deferral schedules the work but does not shrink it.