⚛️ React Hook: useTransition
📖 Quick Summary
useTransition
lets you mark some state updates as non‑urgent so React can keep the UI responsive while rendering them in the background.- It returns
[isPending, startTransition]
. UsestartTransition(() => setState(...))
for low‑priority updates; leave urgent updates (like text input value) as normalsetState(...)
. - Great for filtering large lists, navigating tabs/routes, or any update that could momentarily block typing/scrolling.
- Signature:tsx
const [isPending, startTransition] = useTransition(); startTransition(() => { // one or more state updates here are marked non‑urgent });
🧠 Mental Model
- Think of React 18 rendering as a priority scheduler.
- Urgent updates (typing, clicks) should stay snappy. Heavy updates (filter, recompute, big tree) can be deferred.
useTransition
= “do this soon but don’t block the user right now.” While transition work is rendering,isPending
istrue
so you can show a spinner/skeleton and keep the previous UI visible until the new one is ready.
🔑 Key Concepts
Urgent vs Non‑Urgent Updates
- Urgent: should feel immediate (input’s
value
, button press). - Non‑urgent: can lag slightly (filter results, complex layout).
- Urgent: should feel immediate (input’s
API
const [isPending, startTransition] = useTransition()
- Call
startTransition(() => { setState(...) })
to mark those updates as low‑priority.
UI Behavior
- While the transition is rendering, React keeps the previous UI on screen and
isPending === true
.
- While the transition is rendering, React keeps the previous UI on screen and
Interruptible Rendering
- If new urgent updates arrive, React can interrupt the in‑progress transition and restart with latest data.
Where to Call
- Call
startTransition
inside event handlers or effects, not during render.
- Call
Transitions vs
useDeferredValue
useTransition
: you schedule the non‑urgent setState.useDeferredValue
: you derive a deferred version of a value and render from that.
💻 Code Examples
Example 1: Keep typing responsive while filtering a big list
import { useMemo, useState, useTransition } from "react";
function filterItems(items: string[], query: string) {
// simulate heavy filtering
const q = query.toLowerCase();
const start = performance.now();
while (performance.now() - start < 5) {/* burn 5ms per call */}
return items.filter(i => i.toLowerCase().includes(q));
}
export default function Search({ items }: { items: string[] }) {
const [query, setQuery] = useState("");
const [results, setResults] = useState<string[]>(items);
const [isPending, startTransition] = useTransition();
const onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const next = e.target.value; // URGENT update (keep input snappy)
setQuery(next);
startTransition(() => { // NON‑URGENT update
const filtered = filterItems(items, next);
setResults(filtered);
});
};
return (
<div>
<input value={query} onChange={onChange} placeholder="Search..." />
{isPending && <p>Updating results…</p>}
<ul>
{results.map((r, i) => <li key={i}>{r}</li>)}
</ul>
</div>
);
}
How it works (step‑by‑step):
- User types → we immediately
setQuery(next)
so the input stays responsive. - We wrap the heavy filtering and
setResults
instartTransition
. - React keeps showing the previous results while computing the new ones.
isPending
becomestrue
→ we show a small hint/spinner.- When the transition finishes, the list updates at once.
Example 2: Tab switch that renders a heavy component
import { useState, useTransition } from "react";
function HeavyTab() {
// Pretend this renders a huge tree or runs heavy calculations
const start = performance.now();
while (performance.now() - start < 50) {/* 50ms work */}
return <div>Very heavy content</div>;
}
export default function Tabs() {
const [tab, setTab] = useState<"home" | "stats">("home");
const [isPending, startTransition] = useTransition();
const selectTab = (next: "home" | "stats") => {
startTransition(() => setTab(next)); // non‑urgent: don’t freeze the click
};
return (
<div>
<button onClick={() => selectTab("home")}>Home</button>
<button onClick={() => selectTab("stats")}>Stats</button>
{isPending && <p>Loading tab…</p>}
<div style={{ marginTop: 8 }}>
{tab === "home" ? <div>Welcome!</div> : <HeavyTab />}
</div>
</div>
);
}
How it works (step‑by‑step):
- Clicking a tab triggers
startTransition(() => setTab(next))
. - React can keep the old tab visible while preparing the new one.
isPending
shows a hint; UI remains interactive.- Once ready, React swaps the content with minimal jank.
Example 3: Transition + Suspense (show old screen until data is ready)
import { Suspense, useState, useTransition } from "react";
// imagine <RepoList query={q}/> suspends while fetching
function RepoList({ query }: { query: string }) {
// ... data fetching with a Suspense‑enabled cache
return <div>/* list of repos for {query} */</div>;
}
export default function Explorer() {
const [query, setQuery] = useState("react");
const [isPending, startTransition] = useTransition();
const onSearch = (q: string) => {
startTransition(() => setQuery(q));
};
return (
<div>
<button onClick={() => onSearch("react")}>React</button>
<button onClick={() => onSearch("redux")}>Redux</button>
{isPending && <p>Fetching…</p>}
<Suspense fallback={<p>Loading…</p>}>
<RepoList query={query} />
</Suspense>
</div>
);
}
How it works (step‑by‑step):
- Clicking a button wraps
setQuery
in a transition. - Current
RepoList
stays visible;isPending
is true. - When Suspense resolves for the new query, React swaps the list.
- User never sees a blank screen in between.
Example 4: Combining urgent and non‑urgent updates
import { useState, useTransition } from "react";
export default function MixedUpdates() {
const [text, setText] = useState("");
const [mirrored, setMirrored] = useState("");
const [isPending, startTransition] = useTransition();
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const next = e.target.value;
setText(next); // urgent
startTransition(() => {
// derived or heavy mirror update
setMirrored(next.split("").reverse().join(""));
});
};
return (
<div>
<input value={text} onChange={handleChange} />
{isPending && <small>Updating mirror…</small>}
<p>Mirror: {mirrored}</p>
</div>
);
}
How it works (step‑by‑step):
- Input stays responsive via urgent
setText
. - Mirroring (pretend heavy) is done inside the transition.
- Old mirror is shown until the new one is ready;
isPending
exposes progress.
⚠️ Common Pitfalls & Gotchas
- ❌ Wrapping controlled input value updates in a transition → typing feels laggy. Keep input
setState
urgent. - ❌ Calling
startTransition
during render → must be in an event handler or effect. - ❌ Expecting transitions to fix CPU‑bound blocking by themselves. Heavy synchronous work still blocks the main thread; consider web workers or optimizing logic.
- ❌ Forgetting loading feedback — use
isPending
to show progress/skeleton. - ❌ Using transitions for everything — they are for non‑urgent updates only.
✅ Best Practices
- Keep urgent UI (typing, clicks) as normal
setState
. - Wrap derivations / heavy recomputations / big tree switches in
startTransition
. - Pair with Suspense for data fetching so the previous UI stays while new data loads.
- Consider
useDeferredValue
when you can’t control where state changes, but need a deferred version for rendering. - Profile first; add transitions where they actually improve responsiveness.
❓ Interview Q&A
Q1. What problem does useTransition
solve?
A: It prevents heavy renders from blocking urgent interactions by marking some updates as non‑urgent and letting React keep the previous UI visible.
Q2. What does isPending
indicate?
A: That a transition is currently in progress (React is rendering non‑urgent work). Use it to show progress indicators.
Q3. How is useTransition
different from useDeferredValue
?
A: useTransition
schedules setState at low priority. useDeferredValue
gives a lagging copy of a value for rendering, without changing how the original is updated.
Q4. Can transitions be interrupted?
A: Yes. If a newer urgent update arrives, React can interrupt the in‑flight transition and restart with the latest inputs.
Q5. Where should startTransition
be called?
A: Inside user event handlers or effects (never during render).
Q6. Do transitions make code run in a different thread?
A: No. They are still on the main thread; React just schedules work with different priority.