useTransition & useDeferredValue
Important things now, heavy things right after
With both of these you tell React: “this matters, that can wait.” Typing stays smooth while the heavy list catches up.
Picture typing into a search box – and on every letter React redraws a list of 5,000 entries. The field stutters, because React treats both jobs as equally important.
With these two hooks you say: “the letter has to appear immediately. The list may arrive a few milliseconds later.” React calls that a transition.
// Option A: useTransition – you control the UPDATE
const [isPending, startTransition] = useTransition();
function onType(e) {
setQuery(e.target.value); // urgent: the field
startTransition(() => {
setResults(search(e.target.value)); // may wait
});
}
// Option B: useDeferredValue – you control the VALUE
const [query, setQuery] = useState("");
const laggingQuery = useDeferredValue(query);
// query -> into the input (always current)
// laggingQuery -> into the heavy list (trails a little)
const results = useMemo(() => search(laggingQuery), [laggingQuery]); A waiter takes all the orders first (quick, visible) and only then walks to the kitchen (slow). Without a transition they would walk to the kitchen after every single order – and the next guest waits.
Which of the two?
useTransition ... when you hold the setState call yourself
(switching tabs, applying a filter, navigating)
Bonus: isPending -> you can show a loading hint
useDeferredValue ... when the value arrives as a prop, or you just need
a copy that trails behind
No access to the setter required function Tabs() {
const [tab, setTab] = useState("start");
const [isPending, startTransition] = useTransition();
function switchTo(next: string) {
startTransition(() => setTab(next)); // the click feels instant
}
return (
<>
<button onClick={() => switchTo("reports")}>Reports</button>
<div style={{ opacity: isPending ? 0.6 : 1 }}>
{tab === "reports" ? <HeavyReport /> : <Start />}
</div>
</>
);
} - +
Filtering or sorting large lists while typing
- +
Switching tabs or views that draw a lot
- +
Showing a pending hint without throwing the old view away
- −
The input value itself – the typed text has to appear immediately
- −
As a replacement for debouncing network requests
- −
When nothing actually stutters: measure first, optimise second
startTransition(() => setQuery(e.target.value)) breaks exactly what you set out to fix: the text in the field then appears with a delay. Urgent updates stay outside.
▸ In more depth: what does “may wait” actually mean? optional
Normally React works through an update in one go – and the page does not respond meanwhile. A transition, on the other hand, may be interrupted: if something urgent arrives in between (a key press), React does that first and picks the transition back up afterwards.
So it is a priority, not a delay like setTimeout. Which is also why it does not help against too many network requests – for those you still need DebounceOnly act once things have been quiet for a moment – search after 300 ms without typing, for instance.→ or a query library.
startTransition(async () => { await save(); setStatus("done"); }) keeps isPending true across the whole await. The new form actions build on this.
Does it stick?
4 questions on this lesson. Wrong answers show up in your stats.
Now write it yourself
The list has 3,000 entries and is refiltered on every key press. Make the input respond immediately and let the list catch up – with useDeferredValue.
- □The text in the input appears immediately (
query) - □The list filters by the deferred value
- □While the list trails behind it is slightly dimmed
import { useDeferredValue, useMemo, useState } from "react"; const DATA = Array.from({ length: 3000 }, (_, i) => "Entry " + i); export default function App() { const [query, setQuery] = useState(""); // TODO: derive the deferred value const hits = useMemo( () => DATA.filter((d) => d.includes(query)).slice(0, 20), [query] ); return ( <div style={{ fontFamily: "system-ui", padding: 16 }}> <input aria-label="search" value={query} onChange={(e) => setQuery(e.target.value)} placeholder="e.g. 123" /> <ul data-testid="list"> {hits.map((h) => ( <li key={h}>{h}</li> ))} </ul> </div> ); }