useTransition
Marks state updates inside startTransition as non-urgent so React can keep the UI responsive to more urgent updates. Reach for it when a state change triggers heavy rendering (search results, tab switches) and you want the previous UI to stay interactive.
const [isPending, startTransition] = useTransition() Parameters & Behavior
| Parameter | Purpose |
|---|---|
| isPending | Boolean — true while the transition is in flight; useful for spinners |
| startTransition(fn) | Runs fn synchronously; state updates inside it are flagged as transitions |
| concurrent rendering | React can interrupt the transition to service urgent updates (typing, clicks) |
| no async allowed | In classic useTransition, startTransition's callback must be synchronous |
Examples
const [isPending, startTransition] = useTransition();
const [query, setQuery] = useState('');
const onChange = (e) => {
setQuery(e.target.value);
startTransition(() => setResults(search(e.target.value)));
}; Keep the input responsive while the expensive results list re-renders
startTransition(() => setTab(nextTab));
return isPending ? <Spinner /> : <TabContent tab={tab} />; Show a spinner during a heavy tab switch without blocking clicks
startTransition(() => setFilters(nextFilters)); Filter updates over a large dataset — marked non-urgent so scrolling stays smooth
<button disabled={isPending} onClick={() => startTransition(() => setPage(p + 1))}>Next</button> Disable the button while the transition renders the next page
Gotcha
Only wrap state updates that trigger the expensive rendering — updates outside startTransition stay urgent and can cause tearing. The synchronous startTransition cannot await; use React 19's async actions or startTransition from `react` (not the hook) for those cases.