useDeferredValue: Lagging Values for Smooth UIs

useDeferredValue solves a different problem than useTransition, even though both lean on the same scheduler idea: some updates may trail the latest state so urgent work stays smooth. Where useTransition wraps the function that performs updates, useDeferredValue operates on a value—often a string, an id, or a prop coming from above. That distinction drives when each hook leads to clearer TypeScript and simpler component boundaries.

How deferred values behave

Given a fast-changing value v, useDeferredValue(v) returns a lagging version that React updates with lower priority. During bursts of change, React may keep showing UI derived from the previous deferred value while it prepares a newer one. The result is a deliberate stale-while-rendering experience: the input feels instant; the expensive projection catches up.

function Search({ bigList }: { bigList: string[] }) {
  const [query, setQuery] = useState("")
  const deferredQuery = useDeferredValue(query)

  const results = useMemo(
    () =>
      bigList.filter((item) =>
        item.toLowerCase().includes(deferredQuery.toLowerCase())
      ),
    [bigList, deferredQuery]
  )

  const isStale = query !== deferredQuery

  return (
    <>
      <input value={query} onChange={(e) => setQuery(e.target.value)} />
      <div style={{ opacity: isStale ? 0.5 : 1 }}>
        <ul>
          {results.map((r) => (
            <li key={r}>{r}</li>
          ))}
        </ul>
      </div>
    </>
  )
}

useMemo ties the expensive filter to deferredQuery, not the live query. While query races ahead, deferredQuery follows when the scheduler allows, so the list does not recompute on every keystroke at urgent priority. The isStale flag is the user-visible honesty layer: subtle opacity or a small “catching up” affordance signals that the heavy view may momentarily reflect an older query.

This pattern shines when the costly work is pure derivation from a value you already have. Keep the derivation in useMemo or push it down to a memoized child that receives deferredQuery so React can bail out when props are referentially stable.

useTransition versus useDeferredValue

Both hooks interact with concurrent scheduling, but they optimize different integration points.

useTransition useDeferredValue
What you control The update function The value
How to use Wrap setState (and related updates) in startTransition Pass the live value into the hook
Cancellation semantics In-flight transition renders can be discarded when superseded The hook delays propagation; it does not cancel your derivation logic by itself
Typical ownership You own the state setter in this component You receive a prop or upstream value you cannot wrap
Batching story One transition can cover multiple updates Each deferred value tracks its own lag

When you control the setState, useTransition is often the more explicit choice: you mark exactly which updates are non-urgent. When a parent passes query or searchParams and you cannot ask every caller to wrap transitions, useDeferredValue localizes the decision: this subtree will intentionally lag.

Neither hook replaces algorithmic efficiency. If filtering a million rows is slow, deferral only smooths input; it does not change asymptotic cost. Combine deferred values with pagination, virtualization, or server-side narrowing for real wins.

Markdown preview and live editors

Text areas that drive rich previews are a textbook use case. Keystrokes update source state immediately; preview rendering—parse markdown, sanitize HTML, highlight syntax—runs off a deferred copy.

function MarkdownPreview({ markdown }: { markdown: string }) {
  const deferred = useDeferredValue(markdown)
  const html = useMemo(() => renderMarkdownToHtml(deferred), [deferred])
  const isStale = markdown !== deferred

  return (
    <article
      aria-busy={isStale}
      style={{ opacity: isStale ? 0.85 : 1 }}
      dangerouslySetInnerHTML={{ __html: html }}
    />
  )
}

TypeScript note: if renderMarkdownToHtml returns a branded type or a sanitized wrapper, keep that typing strict so dangerouslySetInnerHTML never receives raw user strings without passing through your sanitizer.

Large tables and dashboards

In data-dense UIs, defer the props that drive heavy columns—sort keys, filters, or time ranges—while keeping tooltips and hover highlights on the urgent path in sibling components. Splitting the tree matters: a deferred value does not help if a single giant component still recomputes everything whenever any parent state changes.

A practical structure:

  • Urgent subtree: controls, focused row highlight, inline editors.
  • Deferred subtree: aggregated charts or wide tables fed by useDeferredValue(filterState).

Measure with the React Profiler plus real hardware. Deferral improves responsiveness; virtualization improves scalability.

Search params and shareable URLs are another common source of “hot” values you do not own. When the router parses ?q= or ?sort= into props, a leaf component can defer those props before passing them into a memoized child. The address bar stays authoritative for the user and for analytics; the heavy table simply lags one beat behind without forcing every route parent to know about startTransition.

You can also push deferral to the child boundary by passing both the live and deferred values explicitly when you need fine control:

function ResultsPane({ query }: { query: string }) {
  const deferredQuery = useDeferredValue(query)
  return <HeavyResults deferredQuery={deferredQuery} liveQuery={query} />
}

HeavyResults can dim itself when liveQuery !== deferredQuery, while still receiving a deferredQuery prop that moves more slowly during bursts of typing—useful when the same component is reused in stories or tests where explicit prop names make expectations clear.

Mental model for code review

Ask whether the lagging UI is safe to show stale. Financial totals that must always match the selected filters should not drift visually without strong disclaimers. Search results and previews usually can. For borderline cases, combine a deferred value with explicit copy: “Results may update momentarily.”

Also remember: isPending from useTransition and the query !== deferredQuery stale flag are related ideas but not identical. Pending spans transitions you wrapped; staleness compares live versus deferred values. Some teams use both in the same screen—transition around a server mutation, deferred value around a local heavy render—so naming variables (isSavePending vs isListStale) keeps reviews readable.

useDeferredValue is the right default when the expensive work is downstream of a value you read, not a callback you write. For mutating flows that need instant feedback and server confirmation, the next section’s useOptimistic layers optimistic state on top of the same scheduling story.