Concurrent Rendering — useTransition, useDeferredValue, How Priorities Work
TL;DR
React 18+ can interrupt rendering to keep the main thread responsive: high-priority updates (typing, clicking) preempt lower-priority ones (large list filter, route transitions). You opt into the deprioritization with useTransition (“this state update is non-urgent”) and useDeferredValue (“give me a lagged version of this value”). They’re INP-critical features — most app slowness comes from one heavy render blocking a fast interaction.
Interview Q&A
Q: What does “concurrent rendering” actually mean?
A: React’s reconciler can start, pause, and resume rendering work. Before React 18, render was synchronous start-to-finish; if it took 200ms, the main thread was blocked for 200ms. Concurrent rendering chunks the work and yields back to the browser between chunks if a higher-priority event arrives.
Implications:
- An expensive
setStatedoesn’t lock the UI. - The user can type/click during long renders without lag.
- React picks up where it left off, or discards the in-progress render if it’s now stale.
Concurrency in React is opt-in via specific APIs (useTransition, useDeferredValue, <Suspense>) — it doesn’t suddenly make all renders interruptible by default.
Q: useTransition — what’s the API and when do you use it?
A:
import { useState, useTransition } from "react";
function Search() {
const [query, setQuery] = useState("");
const [results, setResults] = useState<Result[]>([]);
const [isPending, startTransition] = useTransition();
function onChange(e: React.ChangeEvent<HTMLInputElement>) {
const value = e.target.value;
setQuery(value); // urgent — input stays snappy
startTransition(() => {
setResults(computeResults(value)); // non-urgent — can be interrupted
});
}
return (
<>
<input value={query} onChange={onChange} />
{isPending && <Spinner />}
<List items={results} />
</>
);
}
What happens:
setQueryruns immediately; the input feels responsive.setResultsinsidestartTransitionis marked non-urgent.- If the user types again before
setResults’s render finishes, React discards the in-progress render and starts fresh with the latest value. isPendingis true while the transition’s render is in progress.
Use for: filters, search, route transitions, expensive recomputes triggered by user input.
Q: useDeferredValue — what’s the difference?
A: useDeferredValue(value) returns a value that lags behind the source by a render or two — React renders the urgent change with the old value first, then re-renders with the new value when it has spare cycles.
function Search({ query }: { query: string }) {
const deferredQuery = useDeferredValue(query);
const results = useMemo(() => computeResults(deferredQuery), [deferredQuery]);
const isStale = query !== deferredQuery;
return <List items={results} className={isStale ? "stale" : ""} />;
}
The difference vs useTransition:
useTransitionwraps a state setter — you opt in at the write site.useDeferredValuewraps a value — you opt in at the read site.
Use useDeferredValue when you don’t control the state update (the value comes from props or a hook), or when you want to defer rendering based on a value without restructuring your setState calls.
Q: When are they interchangeable?
A: Often. For a typeahead controlled by your own state:
// Option A — useTransition
const [isPending, start] = useTransition();
function onChange(v: string) { setQuery(v); start(() => setResults(compute(v))); }
// Option B — useDeferredValue
const [query, setQuery] = useState("");
const deferred = useDeferredValue(query);
const results = useMemo(() => compute(deferred), [deferred]);
Both achieve the same effect — non-blocking heavy work, urgent input updates. useTransition gives you isPending; useDeferredValue gives you value !== deferredValue as the stale flag.
Q: What does “priority” mean inside React’s scheduler?
A: React internally tags updates with a priority level. Roughly (simplified):
| Priority | Examples |
|---|---|
| Sync / Discrete | Click, keypress, form input updates |
| Continuous | Scroll, hover, drag |
| Default | setState outside event handlers |
| Transition | startTransition-wrapped updates |
| Idle | (internal) work that can wait |
Higher-priority updates interrupt lower-priority renders. The scheduler yields to the browser between work chunks (~5ms slices) and checks if anything urgent is pending.
You don’t set priorities directly — React infers from API. useTransition is the public knob to demote an update.
Q: How does this connect to INP?
A: INP is the latency from user input → next paint. The bottleneck is usually:
- Event handler runs.
- React renders affected components. (← long if you don’t transition)
- Browser paints.
useTransition shrinks step 2 from the user’s POV: the urgent state (input value) renders fast; the expensive state (filtered list of 5000 items) renders deferred. INP measures the urgent paint, which is now fast.
Without useTransition, both updates batch into one render; the expensive one drags the urgent one along; INP suffers.
Q: Does useTransition work with setState from a non-React source (Redux, Zustand)?
A: Limited. useTransition only marks updates inside its startTransition callback as non-urgent. External stores dispatch synchronously and React schedules their re-renders at the default priority.
For external stores, the equivalent is useDeferredValue(useStore(selector)) — defer the value coming out, even if the store doesn’t know about React’s priorities.
Q: Common mistake — startTransition inside useEffect or async.
A: startTransition is a synchronous wrapper. Calling setState after an await doesn’t get the transition treatment automatically:
// Wrong — startTransition wraps only the await, not the setState after
async function search(q: string) {
startTransition(async () => {
const r = await fetch(...); // awaits here
setResults(r); // this setState is NOT in the transition
});
}
// Right — wrap the setState specifically
async function search(q: string) {
const r = await fetch(...);
startTransition(() => setResults(r));
}
React 19 may relax this for async actions (Server Actions get transition-like behavior). For now, keep startTransition synchronous.
Q: When not to use useTransition?
A:
- The render is already fast (<16ms). Don’t add it preemptively.
- The update is genuinely urgent (the user expects immediate feedback). A confirmed-deletion flash should be sync.
- The state must update atomically with another sync state — splitting via transitions can cause visible inconsistency.
Gotchas / edge cases
isPendingflicker — for sub-100ms transitions,isPendingflashes briefly. Hide the spinner ifpending < 150ms(use a min-display library) or skip it.- Stale
isPendingafter rapid changes — the latest transition’sisPendingwins; the older one’s already discarded. useDeferredValueinitial render — returns the source value on the first render; lag starts from the second update.- Suspense + transition — a suspended boundary inside a transition shows the previous content (not the fallback) until the new one resolves. Great UX: no flash to fallback for navigation.
- Concurrent rendering doesn’t make slow code fast — it lets fast code stay snappy during slow code. Long render is still long.
startTransitionoutside a component (e.g., in an external store) needs to be imported from React directly and called from a context that has access to the renderer; usually youuseTransitioninside components.
What a senior is expected to say
- “Concurrent rendering lets React interrupt low-priority work for high-priority events — the lever for INP-sensitive UIs.”
- “
useTransitionat the write site (startTransition(() => setX(...)));useDeferredValueat the read site for a lagged value. Often interchangeable for typeahead-style flows.” - “The pattern: urgent input update fires immediately and re-renders the input; the expensive downstream update (filter, list render) is wrapped in
startTransitionso it doesn’t block the typing render.” - “Inside an
await,startTransitiondoesn’t carry through — wrap thesetStateafter the await.” - “Useful + visible UX win: Suspense inside a transition keeps the previous content visible while the new view loads (no flash to fallback).”
Cross-references
- INP and how perf is measured: ../15_performance/01_core_web_vitals.md
- Suspense for data (used with transitions): suspense_for_data.md
- Memoization (the static analog): ../15_performance/10_memoization_heuristics.md
Further reading
- React docs —
useTransition: https://react.dev/reference/react/useTransition - React docs —
useDeferredValue: https://react.dev/reference/react/useDeferredValue - React 18 working group — concurrent rendering: https://github.com/reactwg/react-18/discussions