useDeferredValue
Reviewed & published by Brayan K
useDeferredValue is a React Hook that returns a deferred copy of a value, allowing an expensive part of your UI to lag a moment behind a fast-changing input so the input stays responsive while heavy results catch up. You give it a value and React gives you back a version that updates at a lower priority.
Part of the free React course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.
What You'll Learn
- How a deferred value lags behind a fast input
- Passing the deferred value to an expensive child
- Why useDeferredValue pairs with React.memo
- Showing stale results by comparing live vs deferred
- How it differs from useTransition
- When deferring a value beats a transition
1️⃣ The Idea: A Value That Catches Up
Call const deferred = useDeferredValue(value). The live value updates immediately (great for the input), while deferred may briefly hold the previous value during a busy render, then catch up. Pass deferred — not value — to the heavy part.
function Search() {
const [text, setText] = useState("");
const deferredText = useDeferredValue(text);
return (
<>
<input value={text} onChange={(e) => setText(e.target.value)} />
<SlowResults query={deferredText} />
</>
);
}// useDeferredValue lets an EXPENSIVE child lag behind a FAST input.
// We can't run the hook here, so we model the two values it gives you:
// the live value (updates now) and the deferred value (catches up later).
let liveValue = "";
let deferredValue = "";
function type(char) {
liveValue += char;
console.log("input shows:", liveValue); // instant
// React lets the deferred value lag during a busy render…
}
function catchUp() {
deferredValue = liveValue; // …then it catches up
console.log("results use:", deferredValue);
}
type("r"); type("e"); type("a"); type("c"); type("t");
catchUp();
// input shows: r / re / rea / reac / react
// results use: react2️⃣ Pair It With React.memo
Deferring only helps if the expensive child can skip re-rendering when its prop hasn't changed. Wrap that child in React.memo and feed it the deferred value. Now it re-renders only when the deferred value updates — not on every keystroke — which is where the smoothness comes from.
const SlowResults = React.memo(function SlowResults({ query }) {
// heavy filtering/rendering here
const matches = bigList.filter((x) => x.includes(query));
return <ul>{matches.map((m) => <li key={m}>{m}</li>)}</ul>;
});
// Because of memo, SlowResults only re-runs when "query"
// (the deferred value) actually changes.// Detect stale results: if live value and deferred value differ,
// a fresher render is still pending — dim the list to show that.
function isStale(value, deferredValue) {
return value !== deferredValue;
}
console.log(isStale("reac", "rea")); // true -> still catching up
console.log(isStale("react", "react")); // false -> up to date
// In JSX you'd write:
// <div style={{ opacity: isStale(value, deferred) ? 0.5 : 1 }}>3️⃣ Showing Staleness & vs useTransition
When value !== deferredValue, a newer render is pending — dim the list to signal "catching up." The key difference from useTransition: a transition wraps the code that triggers the update (you own the setState), while useDeferredValue wraps a value you already have — perfect when the value is a prop you can't control.
const deferred = useDeferredValue(text);
const isStale = text !== deferred;
return (
<div style={{ opacity: isStale ? 0.5 : 1 }}>
<SlowResults query={deferred} />
</div>
);// 🎯 YOUR TURN — fill in each ___ then press "Try it Yourself".
// 1) staleness is true when value and deferred DON'T match.
// 2) opacity should be 0.5 when stale, else 1.
function isStale(value, deferred) {
return value ___ deferred; // 👉 use !== (not equal)
}
function listOpacity(stale) {
return stale ___ 0.5 ___ 1; // 👉 ternary: stale ? 0.5 : 1
}
console.log(isStale("reac", "rea")); // true
console.log(isStale("go", "go")); // false
console.log(listOpacity(true)); // 0.5
console.log(listOpacity(false)); // 1
// ✅ Expected output:
// true
// false
// 0.5
// 1🧩 Reorder Challenge
These lines build a smooth search box. Put them in the right order:
A. const deferredText = useDeferredValue(text);
B. const [text, setText] = useState("");
C. const isStale = text !== deferredText;
D. <input value={text} onChange={(e) => setText(e.target.value)} />
E. <SlowResults query={deferredText} />Common Errors (and the fix)
- Passing the live value to the heavy child. — Then nothing is deferred. Pass the deferred value to the expensive component.
- Forgetting React.memo. — The child re-renders every keystroke anyway. Memoize it so it skips renders when the deferred prop is unchanged.
- Recreating the filtered array inline without memo. — Expensive recompute each render. Use useMemo keyed on the deferred value.
- Expecting a fixed delay. — It is not a timer. The lag depends on how busy React is; for a fixed wait you still want a debounce.
📋 Quick Reference
| Need | Code |
|---|---|
| Defer a value | const d = useDeferredValue(text) |
| Feed the heavy child | <Slow query={d} /> |
| Skip re-renders | React.memo(Slow) |
| Detect stale | text !== d |
🧠 Quick Recall
1. Which value do you pass to the expensive child?
The deferred value. The live value goes to the input; the lagging deferred value goes to the heavy results.
2. Why does it need React.memo?
So the child skips re-rendering while the deferred prop is unchanged. Without memo it re-renders every keystroke and the lag buys you nothing.
3. useTransition vs useDeferredValue — pick one in a sentence.
Use a transition when you own the setState; use a deferred value when you only have a value (often a prop) flowing in.
Mini-Challenge: Memo-Friendly Filter
Model an expensive filter that only recomputes when the query actually changes, the way a deferred value plus memo behaves. Run it and check your output.
// 🎯 MINI-CHALLENGE: a memo-friendly filter
//
// Expensive children should only recompute when the DEFERRED value
// changes. Write filterFor(query) that returns the matching items,
// and a render counter that only increments when the query is new.
//
// ✅ Expected output:
// matches for "ho": hook,host
// renders: 1
// matches for "ho": hook,host
// renders: 1 (same deferred value -> no extra work)
const items = ["hook", "host", "react", "state"];
let lastQuery = null;
let renders = 0;
// your code here🎉 Lesson Complete
- ✅ useDeferredValue returns a value that lags behind a fast input
- ✅ Pass the deferred value to the expensive child
- ✅ Pair it with React.memo so the child can skip renders
- ✅ Compare live vs deferred to detect and dim stale results
Practice quiz
What does useDeferredValue return?
- A debounced callback
- A memoized component
- A deferred copy of a value that updates at a lower priority, lagging behind during busy renders
- A boolean indicating loading
Answer: A deferred copy of a value that updates at a lower priority, lagging behind during busy renders. You pass it a value and get back a version that may briefly hold the previous value during a busy render, then catch up.
Which value do you pass to the expensive child component?
- The deferred value
- The live value
- Both, alternating
- Neither; you pass a ref
Answer: The deferred value. The live value goes to the fast input; the lagging deferred value goes to the heavy results so they update a beat later.
Why does useDeferredValue pair with React.memo?
- memo is required by the hook
- memo makes the value update faster
- It prevents memory leaks
- So the expensive child can skip re-rendering when its deferred prop is unchanged instead of re-rendering every keystroke
Answer: So the expensive child can skip re-rendering when its deferred prop is unchanged instead of re-rendering every keystroke. Deferring only helps if the child can skip work. Wrapping it in React.memo means it re-renders only when the deferred value actually changes.
How do you detect that results are stale (a newer render is pending)?
- Check a loading boolean
- Compare the live and deferred values: value !== deferredValue means it's catching up
- Use a setTimeout
- Read a ref count
Answer: Compare the live and deferred values: value !== deferredValue means it's catching up. When the live value differs from the deferred one, a fresher render hasn't landed yet — a common pattern is dimming the list while they differ.
Does useDeferredValue debounce with a fixed delay?
- No — it's not a timer; the lag adapts to how busy React is
- Yes, 300ms by default
- Yes, but configurable
- Only in production
Answer: No — it's not a timer; the lag adapts to how busy React is. It lets the deferred value lag during a busy render and catch up when React has spare time. For a fixed wait you'd still use a debounce.
How does useDeferredValue differ from useTransition?
- They are identical
- useTransition only works on the server
- A transition wraps the code that triggers the update (you own the setState); useDeferredValue wraps a value you already have
- useDeferredValue requires Suspense
Answer: A transition wraps the code that triggers the update (you own the setState); useDeferredValue wraps a value you already have. Use a transition when you own the setState. Use a deferred value when you only have a value flowing in — often a prop you can't control.
What's the bug if you pass the live value (not the deferred one) to the heavy child?
- Nothing changes
- Nothing is deferred — the expensive child still updates on every keystroke
- The input stops working
- React throws an error
Answer: Nothing is deferred — the expensive child still updates on every keystroke. Deferring requires feeding the deferred value to the expensive component; passing the live value defeats the entire optimization.
How would you dim stale results in JSX?
- <div hidden={isStale}>
- <div disabled>
- <div className="loading">
- <div style={{ opacity: isStale ? 0.5 : 1 }}>
Answer: <div style={{ opacity: isStale ? 0.5 : 1 }}>. A common pattern sets opacity to 0.5 while value !== deferred, visually signaling that fresher results are on the way.
When is useDeferredValue a better fit than useTransition?
- When you want a fixed delay
- When the slow input is a value (often a prop) you can't wrap the setState for
- When you have no expensive child
- When rendering on the server
Answer: When the slow input is a value (often a prop) you can't wrap the setState for. If you only have a value flowing in and can't control where its setState happens, deferring that value is the right tool.
Besides React.memo, what helps avoid recomputing an expensive filter each render?
- useCallback on the child
- useRef for the result
- useMemo keyed on the deferred value
- Nothing is needed
Answer: useMemo keyed on the deferred value. Wrapping the expensive computation in useMemo keyed on the deferred value avoids recomputing it on unrelated renders.
Continue this course
- Previous: useTransition & Responsive UI
- Next: useSyncExternalStore — Subscribe safely to external stores and browser APIs without tearing
- Quick reference: React cheat sheet
Frequently asked questions
What is the difference between useDeferredValue and useTransition?
useTransition wraps the code that triggers the update — you control where the slow state change happens. useDeferredValue wraps a value you already have (often a prop you can't change). Use a transition when you own the setState; use a deferred value when you only have a value flowing in.
Does useDeferredValue debounce?
Not exactly. It doesn't wait a fixed time. It lets the deferred value lag behind during a busy render and catch up when React has spare time, so the fast part (the input) updates now and the expensive part updates a beat later. The lag adapts to how slow the render is.
Why do I need React.memo with useDeferredValue?
Deferring only helps if the expensive child can skip re-rendering when its prop is unchanged. Wrap that child in React.memo and pass it the deferred value, so it only re-renders when the deferred value actually updates — not on every keystroke.
How do I show that results are stale?
Compare the live value with the deferred one: if they differ, a newer render is pending. Dim the list with an opacity style while value !== deferredValue to signal that fresher results are coming.