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.
| piece | role | analogy |
|---|---|---|
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
| intent | pattern | why |
|---|---|---|
| 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 |