A developer I worked with had wrapped a useTransition call around a simple state update that took under a millisecond to process, expecting it to “make things faster” in some general sense, when in fact useTransition does not speed up any computation at all — it changes the priority and interruptibility of a state update, which is a genuinely different thing, and applying it to work that was never slow in the first place produces no measurable benefit whatsoever.
What useTransition Actually Does
This hook lets you mark a specific state update as a “transition” — meaning React is allowed to interrupt the rendering work caused by this particular update if something more urgent (like a keystroke or a click) comes in, and resume or restart it afterward. It does not make the underlying computation itself execute faster; it changes how React schedules and prioritizes that computation relative to other, more urgent updates competing for the same main thread.
This distinction matters because reaching for useTransition to fix a calculation that is slow in absolute terms (an expensive sort, a heavy data transformation) addresses the wrong layer of the problem — that calculation still takes exactly as long to run, it simply becomes interruptible and lower priority rather than blocking.
const [isPending, startTransition] = useTransition();
function handleChange(value) {
setInputValue(value); // urgent: keep the input responsive
startTransition(() => {
setSearchResults(filterLargeList(value)); // can be deprioritized
});
}
The Specific Problem It Solves
The genuine use case is a render caused by a state update that is expensive enough to block the main thread for a noticeable duration, where a separate, more urgent update (typically reflecting direct user input, like a keystroke) needs to remain responsive during that time. Without useTransition, both updates are treated with equal urgency, meaning the expensive render can visibly block the input from updating smoothly while it processes.
I profiled a filterable list of several thousand items where typing into the search input caused a visible stutter on every keystroke, confirmed in the Performance panel as long main-thread tasks tied directly to the list re-render. Wrapping the list-filtering state update in startTransition measurably restored smooth typing in the input, confirmed by the same Performance panel no longer showing those long blocking tasks tied to keystroke-driven renders.
What isPending Is Actually For
The isPending boolean returned alongside startTransition reflects whether a marked transition is currently in progress, and exists specifically so you can show a corresponding loading indicator for the deprioritized content without blocking the rest of the interface from responding normally in the meantime.
{isPending && <span className="results-updating">Updating results…</span>}
<ResultsList items={searchResults} />
This is worth using directly when a transition is genuinely slow enough that a user could plausibly notice a delay and benefit from a visual cue that something is still processing, rather than wondering whether their input was registered at all.
Why This Doesn’t Help Genuinely Cheap Updates
For state updates that complete in a few milliseconds or less, marking them as a transition provides no measurable benefit, since there is no meaningful blocking duration for React to interrupt in the first place. The hook itself carries a small amount of overhead in tracking transition state, meaning applying it indiscriminately to updates that were never actually slow can introduce overhead without any corresponding gain.
Worth checking directly before reaching for this hook: Using the Performance panel, confirm that the specific state update you are considering wrapping actually produces a long task on the main thread. If it does not, useTransition is solving a problem that does not exist in your specific case.
The Relationship to useDeferredValue
useDeferredValue solves a closely related problem from the other direction — instead of marking the update as a transition, it lets a value lag behind the latest state during urgent renders. The two are often interchangeable depending on whether you control the state update directly (favoring useTransition) or are only consuming a value from elsewhere, such as a prop (favoring useDeferredValue).
A Quick Reference for When to Reach for This Hook
| Situation | Use useTransition? |
|---|---|
| Expensive render caused by a specific state update, confirmed via Performance panel | Yes |
| Update needs to stay responsive to urgent input like keystrokes during that expensive render | Yes |
| State update already completes quickly with no observed blocking | No |
| You only consume a value, without controlling its update directly | Consider useDeferredValue instead |
What Profiling Actually Confirmed
In this specific case, the fix was not making the list-filtering logic itself faster — it remained exactly as expensive as before — but rather changing its scheduling priority so it no longer competed directly with the input’s own urgent update. Confirming this with the Performance panel before and after, rather than assuming the hook had helped based on a subjective sense of smoothness, is what actually validated the change as a genuine fix rather than a placebo.
Are you seeing a specific interaction feel sluggish, like typing or clicking, while something else on the page re-renders? Describe what you’re seeing in the Performance panel and I can help you think through whether useTransition actually fits your case.
🔗 Recommended Reading