useDeferredValue for a jank-free typeahead search
Published August 11, 2026
import { useState, useDeferredValue, useMemo } from "react";
const filterItems = (items: string[], query: string) =>
items.filter((item) => item.toLowerCase().includes(query.toLowerCase()));
export const TypeaheadSearch = ({ items }: { items: string[] }) => {
const [query, setQuery] = useState("");
const deferredQuery = useDeferredValue(query);
const results = useMemo(() => filterItems(items, deferredQuery), [items, deferredQuery]);
const isStale = query !== deferredQuery;
return (
<div style={{ opacity: isStale ? 0.5 : 1 }}>
<input value={query} onChange={(e) => setQuery(e.target.value)} />
<ul>{results.map((item) => <li key={item}>{item}</li>)}</ul>
</div>
);
};What
useDeferredValue lets you keep a fast-changing value (what the user just typed) and a slower "deferred" copy of it around at the same time, so an expensive render driven by that value doesn't block the input itself.
Why it matters
Typing into a search box and filtering a large list on every keystroke competes for the same render: if filtering is expensive, React has to finish it before the input can show the next character, and the input feels laggy. The naive fix — debouncing the state update itself — delays both the input and the results, which isn't what you want; the input should never feel slow. useDeferredValue splits the two: query updates immediately for the input, deferredQuery trails behind it as a lower-priority value that React is free to interrupt and restart if a newer keystroke arrives before the filter finishes.
How it works
queryis regular state, updated synchronously on every keystroke — the<input>always reflects it instantly.deferredQuerywrapsqueryinuseDeferredValue. React renders with the olddeferredQueryfirst (keeping the UI responsive), then re-renders in the background with the new one once it's ready.resultsis memoized offdeferredQuery, notquery, sofilterItemsonly runs against the value that's allowed to lag.isStalecompares the two: while they differ, the list on screen is one keystroke behind, so the snippet dims it viaopacityas a lightweight "still catching up" signal.
Gotchas
useDeferredValuedoesn't makefilterItemsfaster — it changes when the expensive work is allowed to happen, prioritizing the input over the list. If filtering is cheap, you won't notice a difference either way.- Without a second argument, there's no deferred value to show on the very first render. Pass an
initialValue(added in React 19) if you need something other than the real value on mount. - This only helps because
TypeaheadSearchand its expensive consumer render in the same tree. If the list lives in a totally separate component with its own data source, this hook has nothing to defer.
Related
useTransition— same underlying mechanism (marking work as interruptible), but you wrap the update instartTransitioninstead of wrapping the value. Reach foruseTransitionwhen you also need anisPendingflag or you're not just deriving one value from another.useMemo— caches a computation between renders; it doesn't change scheduling priority the wayuseDeferredValuedoes.