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"]
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 }} />;}Which one?
Section titled “Which one?”- Use
useTransitionwhen you own the state update and can wrap it instartTransition— you have the setter at the call site. - Use
useDeferredValuewhen 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.)