skip to content

Transitions and Deferred Values

startTransition and useDeferredValue mark work as non-urgent so React can keep the input responsive and discard renders that are already stale. Interviewers ask what these really do under the hood, and why they are not just a debounce with nicer syntax.

part ofReactoverview, primer and where to startread it →
on this pageshow

explore

questions

4

In a React 19 search box, the onChange handler calls setQuery(value) directly but wraps setFilterTerm(value) in startTransition. Why must the input's own state update stay outside the transition?

level: juniorimportance: must knowfreq 55%

answer

  1. two updates from one keystroke
  2. the field must never lag
  3. transitions are interruptible, inputs are not
  4. only the expensive half is non-urgent

basics

~20 s

The typed value is urgent: React must commit it right away or the characters the user typed appear late. An update marked with startTransition is non-urgent and interruptible, so putting the input's own value in one makes the field feel stuck.

solid answer

~50 s

One keystroke produces two logically different updates. The input's value must be committed immediately, because a controlled input only shows the new character after React commits the state holding it — React's own guidance is that updates to controlled inputs cannot be transitions. The expensive consequence of typing — filtering a large list — is what you mark with `startTransition`, which tells React the update is non-urgent: it may render it in the background, pause part-way, and throw the half-finished render away when the next keystroke arrives. So the split is deliberate: `setQuery` stays urgent so typing stays instant, `setFilterTerm` becomes a transition so the slow list render can be interrupted rather than blocking the keyboard. Marking the input's value as a transition inverts that and produces exactly the lag you were trying to remove.

go deeper

for a junior

Know that one keystroke produces two updates: the field's value, which must be immediate, and the expensive consequence, which can wait. Say plainly that a controlled input's value never belongs inside startTransition.

for a middle

Explain what transition priority actually changes — the render becomes interruptible and discardable, not delayed — and why an interruptible commit makes a controlled field display stale text.

for a senior

Be ready to judge when the split is worth its cost: two sources of truth and a visible lag between field and results are only justified when the downstream render is measurably expensive.

for a principal

Frame it as a responsiveness budget: which updates in a product must be frame-accurate, which may lag, and how the team encodes that policy so priority marking is consistent instead of sprinkled where someone noticed jank.

## What "urgent" means to React React 19 sorts every state update into one of two broad priorities. An update produced directly by a user interaction — typing, clicking, dragging — is **urgent**: React renders and commits it before doing anything else, because the user is watching for the result of their own keystroke. An update scheduled inside `startTransition` is **non-urgent**: React may render it in the background, stop part-way through, and discard the unfinished work if a more important update arrives. The important part is what a transition is *not*. It does not delay the update by a timer, and it does not skip it. The state change is still requested at the moment you call the setter. What changes is that this render loses every race against urgent work. ## One event, two updates A filtered search box produces two conceptually different updates from a single keystroke: ```jsx function Search({ items }) { const [query, setQuery] = useState(''); const [filterTerm, setFilterTerm] = useState(''); function handleChange(e) { setQuery(e.target.value); // urgent: what the field shows startTransition(() => setFilterTerm(e.target.value)); // non-urgent: the slow list } return ( <> <input value={query} onChange={handleChange} /> <BigFilteredList items={items} term={filterTerm} /> </> ); } ``` `query` exists purely so the text field shows what was typed. `filterTerm` exists to drive a potentially thousands-of-rows render. They travel at different speeds on purpose. ## Why the input's value cannot be non-urgent A controlled input displays whatever its `value` prop says. The browser paints the typed character optimistically, but React overwrites the field's value with state on the next commit. If that commit is interruptible, React is free to postpone it while it works on something else, and the field can display text that is a keystroke or two behind the keyboard — the caret jumps, fast typists lose characters visually, and the component feels broken. React's documented rule is direct: do not use a transition for a controlled input's value, because those updates must be applied synchronously. This is also why you cannot "just wrap the whole handler" in `startTransition` and call it done. Everything scheduled inside that callback inherits transition priority, including the input's value. ## What the split actually buys With the split in place, the sequence for one keystroke is: React commits the urgent `query` update immediately, so the character appears; then it starts rendering the list with the new `filterTerm` in the background. If the user types again before that background render finishes, React abandons it and starts over with the newest term. The list never blocks the keyboard, and the user never sees a half-built list — React only commits a transition render once it is complete. While the transition render is outstanding, the previous list stays on screen and stays interactive. That stale-but-usable interval is the point, and `useTransition`'s pending flag exists so you can dim or annotate it. ## Two updates, or one plus a deferred value Holding two pieces of state is not the only shape. If a single `query` state drives both the field and the list, you can keep one urgent state and lag the value the expensive subtree reads instead: ```jsx const [query, setQuery] = useState(''); const deferredQuery = useDeferredValue(query); // <input value={query} …/> renders urgently; <BigFilteredList term={deferredQuery} /> lags behind ``` The division of labour is identical: the field reads the urgent value, the expensive subtree reads the one that is allowed to lag. Choose the two-state form when you already have two setters; choose the deferred form when the expensive component receives a value you do not own the setter for. ## The failure you are avoiding Without any priority split, one keystroke renders the input *and* the huge list synchronously. Because React's render and commit run on the main thread, the browser cannot paint the typed character until that whole render finishes. On a big list that is tens or hundreds of milliseconds per key, which reads as a laggy, sticky text field. Marking only the expensive half as a transition removes the coupling — not by making the list render faster, but by letting the fast update overtake and interrupt the slow one.

  • What happens if you wrap the entire onChange handler body in startTransition instead of just the filter update?
    Every setter called inside that callback inherits transition priority, including the input's own value. The field's displayed text then becomes interruptible and can lag behind the keyboard, which is the exact symptom the split was meant to remove. Only the expensive downstream update belongs in the callback.
  • Does putting an update in a transition mean React might never apply it?
    No. A transition update always lands eventually; React only discards renders that are already stale because a newer update superseded them. When the value changes three times in quick succession, React may abandon two in-progress renders, but the final state is the newest value, committed once.
  • Is there a benefit to the split when the list is small and renders in two milliseconds?
    Essentially none, and it adds a second source of truth plus a possible visible lag between the field and the results. Priority splitting pays off only when the downstream render is expensive enough to be felt; otherwise keep one piece of state and render normally.

saying these in an interview costs you the question

  • Says startTransition delays the update by a timeout
  • Wraps the whole handler, including the input's value
  • Thinks a transition update can be dropped entirely
  • Claims transitions move the render off the main thread
  • Believes the list re-renders faster inside a transition

context

open as a page

A teammate replaces a 300 ms debounce on an expensive filtered list with React 19's startTransition and says the two are equivalent. How do they actually differ in what work runs and what the user sees?

level: middleimportance: must knowfreq 68%

basics

~20 s

A debounce postpones starting the work until the user pauses; a transition starts it immediately but at a priority React can interrupt and discard. So a transition shows fresh results as fast as the machine allows and never inserts an artificial wait, while a debounce is a fixed guess at how long to stall.

open as a page

A React 19 component does `const deferredQuery = useDeferredValue(query)` and renders a large list from deferredQuery. Walk through the renders React performs when query changes from 'ab' to 'abc'.

level: middleimportance: should knowfreq 52%

basics

~20 s

React renders twice. First an urgent render in which the hook still returns the old value, so the fast parts of the UI update while the big list stays as it was; then a background, interruptible render in which the hook returns the new value and the list is rebuilt.

open as a page

You wrapped an expensive list update in React 19's startTransition, but typing in the search field is still visibly janky. What can make a transition fail to restore responsiveness?

level: seniorimportance: should knowfreq 45%

basics

~20 s

Transitions change priority, not cost. React can only yield between units of work, so one component with a very slow render still blocks a frame; and updates can escape the transition — scheduled outside its callback, or forced synchronous — leaving the whole render urgent again.

open as a page