useDeferredValue: Debounce Renders in React
useDeferredValue is a React 19 hook that automatically debounces a value—it returns an old version of the value while React is rendering an expensive update in the background. Unlike useTransition, which you control by wrapping state updates, useDeferredValue works on a value passed to it, delaying that value's updates until React has time to re-render. This is perfect for scenarios where you want the input to feel instant but the expensive side effect (filtering, searching) to be deprioritized.
The hook defers updates to a value without requiring you to manually manage a loading state. React automatically re-renders the component with the deferred value, keeps the old value on screen momentarily, and updates to the new value once the expensive render completes.
useDeferredValue vs. useTransition: When to Use Each
| Scenario | Use This |
|---|---|
| Want to manually control which state updates are transitions | useTransition |
| Want automatic input debouncing without managing state | useDeferredValue |
Need an isPending flag for loading UI | useTransition |
| Want the old value on screen while the new renders | useDeferredValue |
| Filtering based on uncontrolled props from a parent | useDeferredValue |
useDeferredValue is especially useful when you can't wrap the state update in useTransition yourself—for example, when the value comes from a parent component as a prop.
Basic useDeferredValue Pattern
import { useState, useDeferredValue } from 'react';
export function SearchWithDeferred() {
const [query, setQuery] = useState('');
const deferredQuery = useDeferredValue(query);
return (
<div>
<input
type="text"
value={query}
onChange={e => setQuery(e.target.value)}
placeholder="Type to search (input updates instantly)"
/>
{/* This component only re-renders when deferredQuery changes,
not when query changes. React defers rendering this. */}
<SearchResults query={deferredQuery} />
</div>
);
}
function SearchResults({ query }) {
// Expensive filter happens here
const results = expensiveSearch(query);
if (!query) return <p>Enter a search term</p>;
return (
<ul>
{results.map(result => (
<li key={result.id}>{result.title}</li>
))}
</ul>
);
}
function expensiveSearch(query) {
// Simulate an expensive filter over a huge dataset
const allResults = Array.from({ length: 1000000 }, (_, i) => ({
id: i,
title: `Result ${i}`,
}));
return allResults.filter(r =>
r.title.toLowerCase().includes(query.toLowerCase())
);
}
Here's what happens:
- User types in the input—
queryupdates immediately. deferredQuerystays at the old value for now.- React starts rendering
SearchResultswith the new query. - The expensive filter runs in the background as a low-priority render.
- If the user types again before the filter finishes, React abandons the old render and starts a new one with the latest query.
- Once the filter completes without interruption,
deferredQueryupdates and the results show on screen.
Input Stays Instant, Results Are Deferred
The key insight: the input field is tied to the regular query state, so it responds instantly. The expensive child component is tied to deferredQuery, so its re-render is deferred. Users see a responsive input with slightly stale (but not wrong) results, which feels much better than a frozen input.
import { useState, useDeferredValue } from 'react';
export function ProductFilter() {
const [searchTerm, setSearchTerm] = useState('');
const deferredSearchTerm = useDeferredValue(searchTerm);
return (
<div>
<input
type="text"
value={searchTerm}
onChange={e => setSearchTerm(e.target.value)}
placeholder="Search 1 million products..."
/>
{/* Mismatch between searchTerm and deferredSearchTerm
indicates a deferred render is in progress */}
{searchTerm !== deferredSearchTerm && (
<p style={{ color: 'gray', fontSize: '12px' }}>
Filtering (old results showing)...
</p>
)}
<ProductList searchTerm={deferredSearchTerm} />
</div>
);
}
function ProductList({ searchTerm }) {
const products = filterLargeProductDatabase(searchTerm);
return (
<ul>
{products.map(product => (
<li key={product.id}>
{product.name} - ${product.price}
</li>
))}
</ul>
);
}
function filterLargeProductDatabase(term) {
const db = generateProducts(1000000);
return db.filter(p =>
p.name.toLowerCase().includes(term.toLowerCase())
);
}
function generateProducts(count) {
return Array.from({ length: count }, (_, i) => ({
id: i,
name: `Product ${i}`,
price: Math.random() * 1000,
}));
}
Notice the trick: comparing searchTerm !== deferredSearchTerm tells you when a deferred render is in progress. You can use this to show optional UI hints.
useDeferredValue with Props from Parent
useDeferredValue is powerful when you receive a value from a parent as a prop and want to defer updates to that value:
import { useDeferredValue } from 'react';
// Parent component
export function UserProfile({ userId }) {
return <UserDetails userId={userId} />;
}
// Child component
function UserDetails({ userId }) {
// Defer the userId to avoid expensive re-renders
// when the parent rapidly changes userId
const deferredUserId = useDeferredValue(userId);
const user = fetchUserData(deferredUserId);
return (
<div>
<h1>{user.name}</h1>
<p>Email: {user.email}</p>
</div>
);
}
function fetchUserData(userId) {
// Expensive: queries a large API response or computes expensive info
return {
id: userId,
name: `User ${userId}`,
email: `user${userId}@example.com`,
};
}
If the parent changes userId rapidly (e.g., clicking through a list of users), the child won't re-render for every ID. Instead, React defers the updates and renders only the most recent ID once there's browser idle time.
Detecting When a Deferred Render Is Happening
You can check if the value has changed to know if a deferred render is pending:
import { useState, useDeferredValue } from 'react';
export function SearchWithIndicator() {
const [query, setQuery] = useState('');
const deferredQuery = useDeferredValue(query);
const isDeferred = query !== deferredQuery;
return (
<div>
<input
value={query}
onChange={e => setQuery(e.target.value)}
placeholder="Search..."
/>
{isDeferred && (
<div className="spinner" role="status" aria-live="polite">
Searching...
</div>
)}
<SearchResults query={deferredQuery} />
</div>
);
}
function SearchResults({ query }) {
const results = expensiveSearch(query);
return (
<ul>
{results.map(r => <li key={r.id}>{r.title}</li>)}
</ul>
);
}
function expensiveSearch(query) {
return Array.from({ length: 100000 }, (_, i) => ({
id: i,
title: `Result ${i}`,
})).filter(r =>
r.title.toLowerCase().includes(query.toLowerCase())
);
}
The spinner appears while isDeferred is true, disappears once the deferred render completes.
Key Takeaways
useDeferredValueautomatically debounces a value, deferring expensive renders until the browser has time.- Unlike
useTransition, it doesn't require you to wrap state updates; it works on any value. - The input stays responsive because it's tied to the regular value; the expensive re-render uses the deferred value.
- You can detect a deferred render by comparing the original value to the deferred value.
useDeferredValueis ideal when filtering lists, searching, or deferring prop updates from parent components.
Frequently Asked Questions
Is useDeferredValue the same as debouncing with setTimeout?
Not quite. useDeferredValue automatically adapts to the browser's available resources. setTimeout always waits a fixed duration, even if the browser is idle. useDeferredValue is smarter about when to update.
Can I use useDeferredValue with numbers or objects?
Yes. useDeferredValue works with any value type: strings, numbers, objects, arrays. Just pass the value to the hook.
What if the deferred value never updates?
If the expensive render never completes (e.g., infinite loop), the deferred value stays at the old value. In production, the render should always eventually complete, even if it's slow.
Should I use useDeferredValue or startTransition?
Use useDeferredValue for automatic debouncing of values (especially props). Use startTransition when you explicitly control state updates and want an isPending flag.