Priority-based updates β urgent vs non-urgent rendering with startTransition.
useTransition is a React 18 hook that lets you mark state updates as 'non-urgent transitions'. Urgent updates (typing, clicking) get processed immediately and are never interrupted. Transition updates are lower-priority β if an urgent update arrives while a transition is in progress, React interrupts the transition, handles the urgent work, and then restarts the transition. This keeps the UI responsive even during heavy re-renders.
The React scheduler assigns each update a priority lane: SyncLane (urgent, unbatchable), InputContinuousLane (drag/scroll), DefaultLane (normal setState), and TransitionLane (useTransition / startTransition). Higher-priority lanes always preempt lower ones.
Wrapping a setState call in startTransition(callback) tells React: 'this update is a transition β it can be interrupted'. React schedules it in a TransitionLane and keeps the current UI visible until the transition completes.
If the user types another character while a 5000-item list is re-rendering as a transition, React pauses the transition render mid-tree, immediately processes the typing (urgent), updates the input, then restarts the transition from scratch with the new input value.
useTransition returns [isPending, startTransition]. isPending is true while React is processing the transition. Use it to show a subtle loading indicator without blocking the UI.
An alternative to startTransition: useDeferredValue(value) returns a deferred copy of value that lags behind during transitions. The UI renders with the old deferred value first (fast), then re-renders with the new value when React has time.
A low-priority scheduler lane for transition updates. Can be preempted by any higher-priority work (user input, sync updates).
Marks the setState calls inside its callback as non-urgent. Available as a standalone import or from the useTransition hook.
Boolean from useTransition. True while React is still processing the transition update. Use to show a spinner without blocking input.
Returns a deferred copy of a value. Renders with stale value first (instant), re-renders with fresh value when idle. Useful for filtering large lists.
When an urgent update arrives mid-transition, React discards the in-progress transition render and restarts it fresh after handling the urgent work.
1import { useState, useTransition } from 'react';23function SearchPage() {4 const [query, setQuery] = useState('');5 const [results, setResults] = useState(allItems);6 const [isPending, startTransition] = useTransition();78 function handleChange(e) {9 // Urgent: update the input immediately (never interrupted)10 setQuery(e.target.value);1112 // Non-urgent: filter 10,000 items as a transition13 startTransition(() => {14 setResults(allItems.filter(item =>15 item.name.includes(e.target.value)16 ));17 });18 }1920 return (21 <>22 <input value={query} onChange={handleChange} />23 {isPending && <Spinner />} {/* subtle loading indicator */}24 <ResultsList items={results} />25 </>26 );27}
Before transitions, React rendered all state updates at the same priority β a heavy list re-render could freeze the input for hundreds of milliseconds. Transitions let you explicitly tell React 'the user needs the input to feel instant, but the results list can take a moment'. This is the difference between a laggy app and a smooth one when dealing with expensive renders.
A dashboard has tabs: Overview, Analytics, Reports. Clicking 'Analytics' loads heavy chart components. Without transitions, the current tab disappears immediately and the user sees a loading blank until the new tab renders.
The tab switch calls `setActiveTab('analytics')`, which immediately unmounts the Overview tab and starts rendering Analytics. The heavy chart render blocks the main thread for 300ms. During this time, the user sees a blank panel β the old tab is gone, the new tab isn't ready.
Wrap the tab switch in `startTransition(() => setActiveTab('analytics'))`. React keeps showing the Overview tab (marked with `isPending` for a subtle opacity/spinner indicator) while rendering Analytics in the background. When ready, it swaps them in a single frame β no blank state.
Takeaway: useTransition is perfect for tab switching, accordion expansions, and any UI where you want the old content to stay visible while new content prepares. The `isPending` boolean lets you show a subtle loading indicator without destroying the current view.
A search results component receives `query` as a prop and does heavy filtering inline. You want to defer the results rendering but can't wrap the parent's setState in startTransition because you don't control the parent.
The parent component calls `setQuery(e.target.value)` directly. You can't add startTransition because you don't own the parent component (maybe it's from a library). Your heavy ResultsList re-renders on every keystroke because `query` prop changes immediately.
Use `useDeferredValue(query)` in ResultsList: `const deferredQuery = useDeferredValue(query)`. Filter using `deferredQuery` instead of `query`. React returns the old value during heavy renders, showing stale-but-fast results while computing the new filter in the background. Compare `query !== deferredQuery` to show a loading indicator.
Takeaway: useDeferredValue is the consumer-side equivalent of useTransition. Use useTransition when you control the state update (can wrap setState). Use useDeferredValue when you want to defer derived rendering from a prop you don't control.