Skip to content

Transitions

The problem: a slow update blocks a fast one

Section titled “The problem: a slow update blocks a fast one”

Imagine a search box that filters a huge list. Every keystroke updates two things: the input value (must feel instant) and the filtered results (expensive to compute). If both are urgent, the expensive filter blocks the keystroke, and typing stutters.

Transitions let you split them: the keystroke stays urgent, the filtering becomes a low-priority transition that React can interrupt when the next keystroke arrives.

useTransition: mark an update as non-urgent

Section titled “useTransition: mark an update as non-urgent”

useTransition returns [isPending, startTransition]. Wrap the non-urgent state update in startTransition, and use isPending to show a subtle “updating” hint.

function SearchResults({ allItems }) {
const [query, setQuery] = useState("");
const [results, setResults] = useState(allItems);
const [isPending, startTransition] = useTransition();
function handleChange(e) {
setQuery(e.target.value); // urgent — the input updates instantly
startTransition(() => {
// non-urgent — React renders this in the background, interruptible.
setResults(filterExpensive(allItems, e.target.value));
});
}
return (
<>
<input value={query} onChange={handleChange} />
{isPending && <span>updating…</span>}
<List items={results} />
</>
);
}

The keystroke is never blocked: if you type again before the filter finishes, React throws away the in-progress filter render and starts the new one. isPending is true while the transition renders.

flowchart LR
  key["keystroke"] --> urgent["setQuery (urgent)
input updates now"]
  key --> trans["startTransition(setResults)
(non-urgent)"]
  trans --> bg["renders in background
interruptible by next keystroke"]
The urgent update lands first; the transition renders after

useDeferredValue: defer a value instead of an update

Section titled “useDeferredValue: defer a value instead of an update”

useDeferredValue is the same idea from the other direction. Instead of wrapping the update, you wrap the value you read: it returns a version of the value that “lags behind” during urgent work, so the expensive part re-renders off the deferred (stale-for-a-moment) value.

function SearchResults({ allItems, query }) {
const deferredQuery = useDeferredValue(query);
// The list renders against deferredQuery. While the user types, deferredQuery
// stays a step behind, so the expensive list render happens in the background.
const results = filterExpensive(allItems, deferredQuery);
const isStale = query !== deferredQuery;
return <List items={results} style={{ opacity: isStale ? 0.6 : 1 }} />;
}
  • Use useTransition when you own the state update and can wrap it in startTransition — you have the setter at the call site.
  • Use useDeferredValue when the value comes from outside your control (a prop, or state you can’t wrap) — you defer reading it instead.

Both keep urgent input responsive by letting the expensive re-render happen in interruptible background work. Neither makes the expensive work faster — they change its priority, not its cost. (For genuinely huge lists, you still want virtualization.)

What does wrapping a state update in `startTransition` do?
What does `useTransition` return?
When is `useDeferredValue` the better fit over `useTransition`?
Do transitions make the expensive work faster?