Transitions keep typing and clicking responsive during heavy renders and stop already-visible content flashing back to spinners.
Not all updates are equally urgent. Typing into an input must feel instant; re-rendering a big filtered list or switching tabs can take a moment. React's concurrent features let you say so.
`useTransition` returns [isPending, startTransition]. State updates inside startTransition are transitions: low-priority and interruptible.
const [isPending, startTransition] = useTransition();
function selectTab(tab) {
startTransition(() => setTab(tab));
}React renders the transition in the background. If a more urgent update arrives (a keystroke, another click), React abandons the in-progress render and starts again with the latest state. Meanwhile the old UI stays interactive, and isPending lets you dim it or show an inline indicator. If the new content suspends, React keeps showing the previous content instead of replacing it with a Suspense fallback.
In React 19 the function you pass can be async; that's an Action, and isPending stays true until it finishes. State updates after an await currently need their own startTransition wrapper to be marked as transitions.
`useDeferredValue(value)` is the version for when you don't own the state update, for example a value from props. It returns a lagging copy: on an urgent update React first re-renders with the old deferred value, then renders again in the background with the new one.
const deferredQuery = useDeferredValue(query);
<Results query={deferredQuery} /> // memoized, so the urgent render skips itComparing query !== deferredQuery tells you the results are stale. For the benefit, the slow child must be memoized (memo or the Compiler). Transitions cannot control text inputs: the input's own state update must stay urgent.
Example
import { memo, useDeferredValue, useState, useTransition } from "react";
const SlowList = memo(function SlowList({ text }) {
const items = [];
for (let i = 0; i < 300; i++) {
const start = performance.now();
while (performance.now() - start < 1) {} // simulate 1ms of work per row
items.push(<li key={i}>{text} #{i}</li>);
}
return <ul>{items}</ul>;
});
export default function App() {
const [text, setText] = useState("");
const deferredText = useDeferredValue(text);
const [tab, setTab] = useState("about");
const [isPending, startTransition] = useTransition();
return (
<>
<input value={text} onChange={e => setText(e.target.value)} /> {/* stays responsive */}
<div style={{ opacity: text !== deferredText ? 0.5 : 1 }}>
<SlowList text={deferredText} />
</div>
<button onClick={() => startTransition(() => setTab("posts"))}>
Posts {isPending && "(loading...)"}
</button>
<p>Tab: {tab}</p>
</>
);
}Remember
startTransition(() => setX(...))marks updates as interruptible, low priority- Transitions keep old UI visible instead of falling back to Suspense spinners
useDeferredValuegives a lagging copy of a value; pair it with a memoized slow child
Watch out for
- Wrapping a controlled input's own state update in a transition, which makes typing lag.