Some state updates need to feel instant (a key press). Others can take a moment (rendering 10,000 search results). useTransition lets you tell React which is which, so the urgent ones never wait for the non-urgent ones.
The shape
const [isPending, startTransition] = useTransition(). Wrap any state update inside startTransition(() => setX(...)) and React tags it low-priority. The hook tells you if any in-flight transition is still running.
The classic use case — search
Typing in a search input should always feel instant. But updating the results list with a new query might re-render a heavy component tree. Without transitions, typing 'react' would block for tens of milliseconds per keystroke. With a transition wrapping the results update, the input updates immediately (urgent) and the results catch up in the background (non-urgent).
The Suspense interaction
Transitions also smooth Suspense fallbacks. When a state change causes a child to suspend, normally the Suspense fallback flashes in. Inside a transition, React keeps the previous content visible until the new one is ready — no fallback flash. isPending tells you when to show a subtle pending indicator (e.g. a loading bar at the top) without blanking the page.
What goes inside startTransition
Only the state update. Don't wrap fetches, side effects, or anything async. The pattern is: do the work, then mark the state change as a transition:
const data = await fetchResults(query);
startTransition(() => setResults(data));
Priority is communication. React can't infer which updates are urgent — only you know. useTransition is how you tell it. The hooks that exist before transitions (useState, useEffect) treated every update as equally urgent. That assumption is what made React feel janky on big trees.
Code
Search with a transition — input stays responsive·tsx
Build a list filter where typing in an input filters a list of 5000 items. Without useTransition, the input lags as you type. Add useTransition around the filter update and observe: typing stays instant, the filtered results catch up smoothly. Use the isPending flag to dim the list during the transition so the user sees the in-flight state.
Hint
Generate the 5000 items with Array.from({ length: 5000 }, (_, i) => item-${i}). The filter is items.filter(i => i.includes(query)). Without a transition, the per-keystroke filter blocks input.
Progress
Progress is local-only — sign in to sync across devices.