Runnable lesson · press Try it on the examples to edit and run them here.
Some updates are urgent: typing in a box, clicking a button, pressing a key. The user expects an immediate reaction. Others can wait a moment: filtering a huge list, switching to a heavy tab. React lets you tell the two apart, so urgent work is never blocked by slow work.
The problem
If rendering a result takes 200 ms, and it happens on every key press, typing feels sticky, because React finishes the slow render before it can show the next character.
useTransition
useTransition marks a state update as a transition: non-urgent and interruptible. React renders the urgent updates first and keeps the screen responsive. If a new urgent update arrives, React can abandon the old transition render and start again.
const [isPending, startTransition] = useTransition();
startTransition(() => {
setQuery(text); // this update is non-urgent
});
isPending is true while the transition is rendering, so you can show a subtle hint.
// A deliberately slow row, to simulate an expensive list
function SlowRow({ text }) {
const start = performance.now();
while (performance.now() - start < 1) { /* 1 ms of fake work per row */ }
return <li>{text}</li>;
}
const ALL = Array.from({ length: 300 }, (_, i) => "Item " + i);
function App() {
const [text, setText] = React.useState(""); // urgent: the input
const [query, setQuery] = React.useState(""); // non-urgent: the list filter
const [isPending, startTransition] = React.useTransition();
function handleChange(e) {
setText(e.target.value); // updates immediately
startTransition(() => setQuery(e.target.value)); // list updates when it can
}
const matches = ALL.filter((x) => x.toLowerCase().includes(query.toLowerCase()));
return (
<div>
<input aria-label="Filter" placeholder="Type to filter" value={text} onChange={handleChange} />
<p>{isPending ? "Updating list..." : "Matches: " + matches.length}</p>
<ul style={{ opacity: isPending ? 0.5 : 1 }}>
{matches.map((x) => <SlowRow key={x} text={x} />)}
</ul>
</div>
);
}
The input state (text) updates at once, so typing feels fine. The list state (query) updates in a transition. While the slow list renders, the old list stays visible (dimmed) and isPending is true.
Rules for transitions:
- Wrap state updates (the setter calls), not the whole handler logic.
- You cannot use a transition to control a text input’s own value. Use a normal update for the input and a transition for the expensive state.
- A transition only helps when there is expensive rendering to postpone. For network waiting it keeps old content visible while Suspense loads (Suspense).
useDeferredValue
When you do not control the code that sets the state (a prop from a parent, a library), use useDeferredValue. It gives you a lagging copy of a value: during urgent updates it keeps the old value, then catches up in the background.
function Results({ query }) {
const items = Array.from({ length: 50 }, (_, i) => `${query} result ${i + 1}`);
return (
<ul>
{items.map((x) => {
const start = performance.now();
while (performance.now() - start < 1) { /* slow */ }
return <li key={x}>{x}</li>;
})}
</ul>
);
}
function App() {
const [query, setQuery] = React.useState("");
const deferredQuery = React.useDeferredValue(query); // lags behind during fast typing
const stale = query !== deferredQuery;
return (
<div>
<input aria-label="Search" placeholder="Type here" value={query} onChange={(e) => setQuery(e.target.value)} />
<p>Showing results for: {deferredQuery}</p>
<div style={{ opacity: stale ? 0.5 : 1 }}>
<Results query={deferredQuery} />
</div>
</div>
);
}
query updates immediately (the input stays responsive); deferredQuery updates after the urgent work is done, and Results renders with it. If the user keeps typing, React skips outdated renders.
useTransition | useDeferredValue | |
|---|---|---|
| You control | The code that sets state | A value you receive |
| Gives you | isPending and startTransition | A deferred copy of the value |
| Typical use | Tab switches, filters, navigation | Search results driven by a prop |
Do not use them as debouncing
Transitions do not wait a fixed time; they run as soon as there is capacity, so they do not reduce the number of network requests. For that, debounce the input (Search and Filtering).
When not to bother
If the list is small, nothing is slow and these hooks add complexity for no gain. Use them after you measure a real delay (Performance Optimization).
Key points
- Transitions mark non-urgent updates so urgent ones (typing) stay smooth.
useTransitiongivesstartTransitionandisPending;useDeferredValuegives a lagging copy of a value.- Use them only for measured, expensive renders; they do not reduce network requests.