Ch. 3 · React

React Transitions and Urgent Input Updates

useTransition explained: keep typing responsive by marking heavy renders as non-urgent, why input state stays urgent, and useDeferredValue.

~6 min readintermediateupdated Oct 6, 2026

“Typing in this filter box lags because the list below re-renders thousands of rows. How do you fix it without debouncing?” Since React 18, the expected answer involves useTransition or useDeferredValue. The interviewer then checks the details: which state update goes inside the transition, which must not, and what the user sees while the slow render is in progress.

This topic tests whether you understand concurrent rendering as a scheduling tool, not a magic speed-up. Transitions do not make rendering faster; they let React keep the important updates responsive while slower ones happen in the background.

Before you start

You should know how state updates trigger re-renders, how a controlled input works, and basic profiling with React DevTools. Transitions were introduced in React 18. React 19 extended them: the function passed to startTransition can be async, and pending state then covers the whole async action, which is what powers useActionState and form actions.

The short answer

React distinguishes urgent updates, like typing, clicking or pressing keys, which must reflect immediately, from transition updates, like rendering filtered results or switching tabs, which can lag a little. Wrapping a state update in startTransition marks it as non-urgent: React renders it in the background, interrupts that render if urgent input arrives, and keeps showing the current UI until it finishes. Keep the input’s own state outside the transition so the text field never lags, put the expensive derived state inside, and use isPending to show a subtle loading indicator.

How it works

Without transitions, every update is processed at the same priority, and a render cannot be interrupted once started. If rendering the list takes 200 ms, each keystroke blocks the input for 200 ms.

With two pieces of state, one urgent and one deferred, React can render them separately:

function Search({ items }) {
  const [text, setText] = useState('');
  const [query, setQuery] = useState('');
  const [isPending, startTransition] = useTransition();

  function onChange(e) {
    setText(e.target.value);              // urgent: input updates now
    startTransition(() => {
      setQuery(e.target.value);           // non-urgent: list can wait
    });
  }

  return (
    <>
      <input value={text} onChange={onChange} />
      <div style={{ opacity: isPending ? 0.6 : 1 }}>
        <SlowList items={items} query={query} />
      </div>
    </>
  );
}
JSX

On each keystroke, React first commits the urgent update so the input shows the new character. It then starts rendering SlowList with the new query. If another keystroke arrives mid-render, React abandons the stale render, handles the new input, and starts the transition again with the latest value. React yields to the browser periodically during transition renders, which is what keeps typing smooth.

SlowList should be wrapped in memo so that urgent renders, where only text changed, skip it entirely.

Step-by-step walkthrough

Step 1: Profile to confirm rendering is the bottleneck

Record a few keystrokes in the React DevTools Profiler. If the list’s render dominates each commit, transitions can help. If the cost is a network request or a slow effect, transitions are not the right tool.

Step 2: Split urgent and non-urgent state

Identify the value the user directly manipulates (the input text) and the value that drives the expensive output (the query used for filtering). They start identical but are updated at different priorities.

Step 3: Wrap only the non-urgent setter

The callback passed to startTransition must call the setter synchronously (or, in React 19, inside an async action, with updates after an await wrapped in another startTransition). Code that does not set state gains nothing from being inside it.

Step 4: Show pending state without hiding content

isPending is true from the moment the transition starts until it commits. Dim the list, show a small spinner next to the input, or both. Do not replace the list with a fallback, because the point of the transition is that the previous results stay usable.

Step 5: Consider useDeferredValue when you do not own the setter

If a component receives the value as a prop, it can defer it itself:

function Results({ query }) {
  const deferredQuery = useDeferredValue(query);
  const isStale = query !== deferredQuery;
  return (
    <div style={{ opacity: isStale ? 0.6 : 1 }}>
      <SlowList query={deferredQuery} />
    </div>
  );
}
JSX

React first re-renders with the old deferred value (cheap, because the memoised list is skipped), then renders the new value in the background.

Worked scenario

A team wrapped the entire input handler in a transition, expecting everything to get faster:

<input
  value={text}
  onChange={e => startTransition(() => setText(e.target.value))}
/>
JSX

The input became worse: characters appeared late, and on fast typing the cursor jumped to the end. A controlled input must be updated synchronously with the event, otherwise the DOM value and React’s state disagree between keystrokes. React’s documentation warns explicitly against using transitions for controlled input state.

The fix is the split from the How it works section: setText urgent, setQuery inside the transition. After the change, the Profiler showed short urgent commits under 5 ms for each keystroke and longer, interruptible list renders that no longer blocked typing.

A second improvement came from asking why the list was slow at all: each row formatted dates with a new Intl.DateTimeFormat instance. Hoisting the formatter cut render time by more than half. Transitions keep the UI responsive; they do not replace fixing slow components.

Common mistake

  • Wrapping the input’s own state in a transition. It makes typing lag and can break the cursor.
  • Expecting transitions to speed up rendering. Total work is the same or slightly more; only priority changes.
  • Using transitions for network latency alone. In React 18, a transition only covers the synchronous state update. In React 19 an async transition can cover a request, but caching and Suspense still decide what is shown.
  • Forgetting memo on the slow child. Without it, the urgent render also re-renders the expensive list, and the benefit disappears.
  • Replacing debouncing everywhere. Debouncing still makes sense to limit network requests; transitions address render cost.

Verify the behavior

Build an artificially slow list and compare:

const SlowItem = memo(function SlowItem({ text }) {
  const start = performance.now();
  while (performance.now() - start < 1) {} // 1 ms per item
  return <li>{text}</li>;
});

const SlowList = memo(function SlowList({ query }) {
  const items = Array.from({ length: 250 }, (_, i) => `${query} ${i}`);
  return <ul>{items.map(t => <SlowItem key={t} text={t} />)}</ul>;
});
JSX

Render it with query={text} and type quickly: the input freezes for about 250 ms per character. Switch to the transition version and type again: characters appear instantly, and the list catches up when you pause. The Performance panel shows the transition render split into small tasks instead of one long task.

Follow-up questions

What happens if a transition suspends? React keeps showing the previous content instead of the nearest Suspense fallback, and isPending stays true. See Suspense boundaries.

What changed in React 19? startTransition accepts async functions (actions), so isPending stays true through an awaited request. State updates after an await still need their own startTransition wrapper, a known limitation.

Can I use startTransition without the hook? Yes, React exports a standalone startTransition for code outside components, but it does not give you isPending.

useTransition or useDeferredValue? Use useTransition when you control the state update; use useDeferredValue when you only receive the value. The deferred results feedback note compares their UI patterns.

Interview exercise

A tabbed settings page renders a very heavy “Advanced” tab. Clicking from “General” to “Advanced” freezes the page for a second, and if the user changes their mind and clicks “Privacy” during that time, nothing happens until the freeze ends. Fix it and describe what the user sees.

Answer and reasoning

Wrap the tab change in a transition: startTransition(() => setTab(next)). Clicking “Advanced” starts an interruptible background render while the “General” tab stays visible and interactive. If the user clicks “Privacy” during that render, React discards the unfinished “Advanced” work and renders “Privacy” instead, so the page responds to the latest choice. Use isPending to dim the current tab or show a spinner on the clicked tab button so the user knows something is happening. If “Advanced” loads code or data with Suspense, the transition also prevents the current tab from being replaced by a fallback.

Continue learning

Keep practising with the React interview questions and the React MCQs. Related notes: deferred results feedback, Suspense boundaries and useActionState forms. Official reference: the useTransition reference and useDeferredValue reference on react.dev.

More in React

read ✓React · mid

React Deferred Results and Stale UI

Keep a search input responsive with useDeferredValue, show users when results are stale, and see why memo is required and fetches aren't reduced.

~6 min readread →
read ✓React · hard

React Context Splitting for Performance

Why every React context consumer re-renders when any part of the value changes, and how splitting state, actions and slow-changing data fixes it.

~6 min readread →
read ✓React · mid

React lazy Loading and Code Splitting

Split React bundles with lazy and Suspense, preload chunks before the click, and handle the chunk-load failures that follow every deploy.

~6 min readread →
esc