The Problem: Slow Updates Block the UI
By default, every state update is treated as urgent — React tries to apply it right away. If that update triggers an expensive re-render (like re-filtering a huge list), the UI can feel like it’s freezing, since React doesn’t interrupt that work to keep something else (like a text input) feeling responsive.
What useTransition Does
useTransition lets you mark a state update as a "transition" — lower priority than urgent updates like typing — so React can keep the UI responsive and interrupt the transition’s work if something more urgent comes in.
A Search Box That Stays Responsive
import { useState, useTransition } from "react";
function SearchPage({ allItems }) {
const [query, setQuery] = useState("");
const [results, setResults] = useState(allItems);
const [isPending, startTransition] = useTransition();
function handleChange(event) {
const value = event.target.value;
setQuery(value); // urgent: keeps the input feeling instant
startTransition(() => {
// low-priority: filtering a huge list won't block typing
setResults(allItems.filter((item) => item.includes(value)));
});
}
return (
<>
<input value={query} onChange={handleChange} />
{isPending && <p>Updating results...</p>}
<ResultsList items={results} />
</>
);
}useTransition Return Value
| Item | Purpose |
|---|---|
| isPending | true while the transition is still processing in the background |
| startTransition | Wraps a state update, marking it as low-priority |
Transitions Can Be Interrupted
If the user keeps typing while a transition is still processing an older value, React abandons the stale, in-progress transition and starts a new one with the latest value — the UI never falls behind or shows outdated results from an interrupted update.
startTransition Only Wraps State Updates
Only the state updates called synchronously inside the function passed to startTransition are treated as a transition. An async operation like a fetch call inside it is not itself part of the transition — only the setState calls that happen after it resolves.
useTransition vs useDeferredValue
useTransition marks the *update itself* as low priority (used when you control the event that triggers the update, like an onChange). useDeferredValue instead takes an already-changing value and lazily defers using it (useful when you don’t control where the value comes from, like a prop).
Best Practice
Reach for useTransition when a specific state update you control (in an event handler) triggers an expensive re-render, and you want the rest of the UI to stay interactive while it processes.