useTransition — urgent vs non-urgent updates

[check: …]
📖 guide (.md) ← react deep dive
📖 Pair this live React playground with the companion guide (.md) — this page is the rendered ground truth. ↗ Builds on frontend/react: useState & hooks (the synchronous render model this makes concurrent).

synchronous → concurrent: split updates by urgency

By default every setState is urgent — React must render it before the browser can paint the next frame. That blocks typing when the render is heavy. useTransition returns a pair [isPending, startTransition]: wrap the expensive setState in startTransition(fn) and React schedules it at transition priority — interruptible, deferred, and shown only when ready. The urgent update (the input) paints immediately; the non-urgent update (the filtered list) catches up in the background.

pieceroleanalogy
startTransition runs a callback whose setStates are marked low priority the "later" tray — React processes it when the urgent queue is empty
isPending true while the deferred render is computing the loading light — on while the background work is in flight
urgent setState normal setState outside startTransition the "now" tray — must paint before the next frame
interruption a new urgent update cancels the in-progress transition an interrupting phone call — drop the tray, handle the call, resume after

1 · the transition you write (edit me)

2 · Babel compiles JSX → element tree

<TransitionDemo/> becomes React.createElement(TransitionDemo). setInput runs at urgent priority (input updates now); setFilter inside startTransition runs at transition priority (list updates when React is idle). isPending flips true the moment the transition is scheduled.

// (hit "compile & render" to see Babel's output)

3 · live React (urgent vs non-urgent, proven)

Type fast in the box. The input updates every keystroke (urgent); the list is re-filtered inside a transition, so it dims and catches up — the spinner shows while isPending is true. The gold-check does this automatically: types "Item 1", asserts the input is instant, waits for the list to shrink, and confirms the pending indicator fired.

items: · pending: · gold: 200 items → type "Item 1" → list shrinks, pending observed

intent → pattern

intentpatternwhy
keep typing responsive while a heavy list filters setInput(v); startTransition(() => setFilter(v)) input is urgent (paints now); filter is non-urgent (deferred)
show a spinner during the deferred work {isPending && <Spinner/>} isPending is true until the transition commits
switch tabs without blocking the click startTransition(() => setTab(next)) the old tab stays interactive until the new one is ready
you control the value, not the update use useDeferredValue instead defer at the consumption site (read-side), not the producer
urgent work must never be wrapped keep typing/clicking/scroll setState OUTSIDE startTransition wrapping urgent updates makes the UI feel frozen