Concurrent Features
React 18 interruptible rendering with useTransition, useDeferredValue, and startTransition to keep the UI responsive.
The core idea#
React 18 introduced concurrent rendering: rendering work is now interruptible. React can start rendering an update, pause to handle a more urgent one (like typing), then resume — instead of blocking the main thread until a big render finishes. You opt in by marking some updates as non-urgent (transitions). 🎯
Two update priorities:
| Priority | Examples | Behaviour |
|---|---|---|
| Urgent | Typing, clicks, hovers | Applied immediately, never interrupted |
| Transition | Filtering a big list, tab switch, search results | Rendered in the background; interruptible |
useTransition#
Returns [isPending, startTransition]. Wrap the state update that triggers heavy work so it doesn't block the urgent input update.
function Search({ allItems }) {
const [query, setQuery] = useState("");
const [results, setResults] = useState(allItems);
const [isPending, startTransition] = useTransition();
function onChange(e) {
setQuery(e.target.value); // urgent: input stays snappy
startTransition(() => { // non-urgent: heavy filter
setResults(allItems.filter((i) => i.name.includes(e.target.value)));
});
}
return (
<>
<input value={query} onChange={onChange} />
{isPending && <Spinner />}
<List items={results} />
</>
);
}
useDeferredValue#
Defers a value instead of wrapping an updater — useful when the expensive consumer is a child and you don't own the setState. React keeps showing the old value while re-rendering with the new one in the background.
function App({ text }) {
const deferredText = useDeferredValue(text); // lags behind during heavy renders
return <SlowList text={deferredText} />; // memoize SlowList to skip work
}
⚠️ useDeferredValue only helps if the heavy child is memoized (React.memo); otherwise it re-renders every time anyway.
startTransition (standalone)#
The non-Hook version for use outside components (no isPending), e.g. in a router or event handler.
import { startTransition } from "react";
startTransition(() => setPage("dashboard"));
When to use: keep an input responsive while a large derived list, chart, or tab re-renders. Do not wrap the state that drives a controlled input itself in a transition — the input value must be urgent, or typing feels laggy.
flowchart TD
A["User types in input"] --> B["setQuery — URGENT"]
B --> C["Input updates immediately"]
A --> D["startTransition(setResults) — non-urgent"]
D --> E["Heavy list renders in background"]
E --> F{"New keystroke arrives?"}
F -->|"Yes"| G["React interrupts, restarts with fresh input"]
F -->|"No"| H["Commit list, isPending -> false"]
Interview Q&A#
Q1. What does "concurrent rendering" actually mean? Rendering is interruptible. React can begin a render, yield to handle higher-priority updates (like user input), and resume or discard the in-progress render. It's a capability, not a mode you turn on globally — features like transitions opt into it.
Q2. useTransition vs useDeferredValue?
useTransition wraps the state updater you control and gives you isPending. useDeferredValue wraps a value you receive (e.g. a prop) when you can't wrap its setter. Both downgrade work to non-urgent; pick based on whether you own the update.
Q3. Why shouldn't you wrap a controlled input's own value update in a transition?
The input value must reflect keystrokes instantly. Transitions are interruptible and may lag, making typing feel broken. Keep setQuery urgent; mark only the expensive derived update as a transition.
Q4. Does useDeferredValue make a slow component fast? No — it defers when the component receives the new value, letting the old UI stay interactive. The heavy child must be memoized so it can skip re-rendering with the stale value; otherwise there's no benefit.