10LOC
Reactintermediate

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

  • query is regular state, updated synchronously on every keystroke — the <input> always reflects it instantly.
  • deferredQuery wraps query in useDeferredValue. React renders with the old deferredQuery first (keeping the UI responsive), then re-renders in the background with the new one once it's ready.
  • results is memoized off deferredQuery, not query, so filterItems only runs against the value that's allowed to lag.
  • isStale compares the two: while they differ, the list on screen is one keystroke behind, so the snippet dims it via opacity as a lightweight "still catching up" signal.

Gotchas

  • useDeferredValue doesn't make filterItems faster — 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 TypeaheadSearch and 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 in startTransition instead of wrapping the value. Reach for useTransition when you also need an isPending flag or you're not just deriving one value from another.
  • useMemo — caches a computation between renders; it doesn't change scheduling priority the way useDeferredValue does.