Lesson 67 of 80
React 18 didn't add one feature — it added an interruptible renderer, and everything else (batching, transitions, deferred values) is a consequence of that one change.
12 min read
Every option explains itself — why the right one is right, and why each wrong one is wrong. Miss one and it comes back later on a spaced-repetition schedule.
This topic is about judgment and practical tradeoffs more than a single right answer — a mock interview is the better way to test yourself on it.
Be interviewed on this →Hints without spoilers, and a plain explanation of why a wrong answer was wrong — answered only from this lesson. Free account, no card.
Sign up freeWhat interviewers actually ask about this, and what a strong answer has to cover.
A strong answer covers
Likely follow-ups
It's easy to learn useTransition, useDeferredValue, and "automatic batching" as three separate new hooks/behaviors to memorize. They're really one underlying change wearing three different hats: React 18 made rendering interruptible, and each of these is a specific way of taking advantage of that.
None of it turns on by accident, either — it's gated behind opting into the new root API:
// React 17 and earlier — the legacy, non-concurrent root
import ReactDOM from "react-dom";
ReactDOM.render(<App />, document.getElementById("root"));
// React 18 — opts into concurrent features
import { createRoot } from "react-dom/client";
const root = createRoot(document.getElementById("root"));
root.render(<App />);
Swap ReactDOM.render for createRoot(...).render, and the app is running on the concurrent renderer — without that swap, none of what follows applies, even on React 18.
Before React 18, React only batched multiple setState calls inside its own event handlers. Anything outside that — a setTimeout callback, a raw promise .then(), a native event listener — triggered one re-render per call:
function handleClick() {
setTimeout(() => {
setCount((c) => c + 1); // React 17: separate re-render
setFlag((f) => !f); // React 17: another separate re-render
// React 18 with createRoot: both batched into one re-render
}, 0);
}
That's automatic batching — not a new API to call, just the existing batching behavior now applying everywhere instead of only inside React's own event handlers. It's a straightforward win: fewer re-renders for the same code, no code changes required beyond using createRoot.
useTransition and useDeferredValue are the other side of the same coin — they let you mark specific updates as low-priority, so React can interrupt them for something more urgent (a keystroke) instead of finishing them first. Both are covered in depth in their own lessons in this track; the thing worth holding onto here is that they only exist because rendering became interruptible — they're consequences of the same underlying architecture change, not unrelated new hooks that happened to ship in the same release.
setState call inside a non-React callback (assuming it applied immediately, pre-React-18) will now see the batched, deferred value instead — worth checking for on an upgrade, not just assuming it's a free win.useTransition and useDeferredValue solve the same kind of problem from two different angles, not two unrelated features — one wraps the state update you control, the other wraps a value you're already receiving (like a prop) and don't control the update for.The interruptible renderer this lesson describes is exactly what an earlier lesson in this track covers at the data-structure level — the change that let React's reconciliation walk pause and resume instead of running as one uninterruptible pass. Everything here — automatic batching, transitions, deferred values — is what became possible once that change shipped; this lesson is the tour of the payoff, not a second explanation of the mechanism underneath it.