React 18 concurrent features
React 18+ can render in the background, interrupt itself, and prioritize urgent updates. The primitives: `useTransition` / `startTransition` (mark non-urgent updates), `useDeferredValue` (lag a value to keep input responsive), Suspense for data + code streaming, and automatic batching across async boundaries. They don't make React faster — they let you schedule work so that user input always wins.
Pre-18 React was synchronous. setState started a render that ran to completion before the browser could repaint. Slow renders → janky inputs. React 18 introduces a concurrent renderer: render work can be paused, resumed, and discarded. Combined with new primitives, you can mark which updates are urgent vs. interruptible.
The four big primitives
1. useTransition / startTransition.
Mark a state update as non-urgent. The transition can be interrupted by urgent updates (typing, clicks).
const [isPending, startTransition] = useTransition();
const [query, setQuery] = useState("");
const [results, setResults] = useState([]);
function handleChange(e) {
const next = e.target.value;
setQuery(next); // urgent — input updates immediately
startTransition(() => {
setResults(expensiveSearch(next)); // non-urgent — can be interrupted
});
}If the user keeps typing, React abandons the in-progress search render and starts a new one. The input stays responsive. isPending lets you show a subtle "filtering…" indicator without blocking.
Use for: filtering large lists, switching tabs that render expensive panels, navigation in client-side routing, form submission that triggers data refetch.
2. useDeferredValue.
A "consumed" version of a value that lags behind the source. Use when you can't wrap the setter in a transition (because someone else owns the state).
function SearchResults({ query }) {
const deferred = useDeferredValue(query);
// `deferred` updates after urgent renders settle
return <ResultList query={deferred} />;
}The parent updates query urgently; the child re-renders with a deferred copy. The child's render is interruptible.
Like a built-in debouncer, but timing-free — it adapts to render cost. Pair with React.memo so the child can bail out when the deferred value equals the previous.
3. Suspense for code and data.
Suspense boundaries let a component "suspend" (throw a promise). React shows the fallback while the promise settles, then renders the component. Combined with streaming SSR, content can appear progressively:
<Suspense fallback={<Skeleton />}>
<SlowChart />
</Suspense>Works with: React.lazy (code splitting), data fetching frameworks (Next.js, Relay, TanStack Query's useSuspenseQuery), and the new use() hook in 19+.
4. Automatic batching.
In React 17, multiple setStates inside an event handler batched, but inside async (setTimeout, fetch) they didn't — each triggered a separate render. React 18 batches across all contexts.
async function onSubmit() {
setIsSubmitting(true);
const data = await api.post();
// In 17: two re-renders here. In 18: batched into one.
setIsSubmitting(false);
setData(data);
}flushSync is the escape hatch when you need to opt out (rare — e.g., DOM measurement between updates).
The mental shift
- Urgent updates: input typing, hover, click feedback. Must complete before the next paint.
- Transitions: results of those updates that take time. Can be interrupted, deferred, or dropped if superseded.
Saying "mark this update as a transition" is saying "the user doesn't need this to be immediate; what they need immediate is the cause (their input), not the effect (the search results)."
What it doesn't do
- Doesn't make code faster. A 100ms render is still 100ms. It just doesn't block input handling for 100ms.
- Doesn't replace memoization. Transitions + slow components without
memostill re-render a lot. - Doesn't help if your bottleneck is the main thread doing non-React work. Heavy synchronous JS during render still blocks.
Strict Mode interaction
Strict Mode in development renders effects and components twice (with state preserved). It catches:
- Side effects in render (which break under interruption + retry).
- Effects that don't clean up properly.
Concurrent rendering can re-run renders; impure components will misbehave. Strict Mode flushes these out before production.
<Activity> (formerly <Offscreen>) — coming in 19+
Lets you render a subtree in the background, keeping its state, without painting. Useful for: tab interfaces (pre-render the other tab), route prefetch + retain state.
Common pitfalls
- Wrapping urgent state in a transition. The input lags. Only mark the consequences as transitions, not the cause.
- Side effects inside startTransition. Side effects don't get the interruption / deferral magic — they fire eagerly. Move side effects to
useEffectkeyed off the deferred state. - Suspense at the wrong level. A Suspense boundary that wraps the whole page falls back to a giant spinner on every refetch — too coarse. Wrap the smallest unit that has its own loading meaning.
useDeferredValuewithout memo'd children. The whole point is the child re-renders less — if it re-renders anyway due to other props, you've gained nothing.
Senior framing
The interviewer expects:
- Naming the four primitives with one-line summaries.
- Clear use-cases — what each is for in production code.
- Mental model: urgent vs transition.
- What's NOT solved — concurrent rendering doesn't speed up slow code.
- Interaction with Strict Mode — pure render functions matter more now.
- Suspense boundaries placement strategy.
The candidate who lists the APIs is mid. The one who can explain why an input feels janky and how transitions fix it is senior.
Follow-up questions
- •Difference between `useTransition` and `useDeferredValue` in practice?
- •Why does concurrent rendering require pure render functions?
- •How does automatic batching change error handling in async code?
- •When does Suspense at the wrong level hurt UX?
Common mistakes
- •Wrapping urgent state in a transition.
- •Putting side effects inside startTransition.
- •Coarse Suspense boundaries.
- •Assuming concurrent rendering makes slow code fast.
Performance considerations
- •Transitions can interrupt expensive renders — the work isn't wasted if React reuses partial results.
- •useDeferredValue is essentially free unless children re-render.
- •Suspense streaming improves TTFB / FCP for SSR.
Edge cases
- •External stores (Redux, Zustand) need useSyncExternalStore for tearing-free concurrent rendering.
- •Strict Mode double-renders catch impurity bugs.
- •Refs across transitions — the ref may point to the pre-transition DOM.
Real-world examples
- •Tab switchers, search results in a sidebar, route transitions in client routers.
- •Next.js App Router uses transitions for navigation.