createRoot() vs render()
1. The main difference
| Feature | ReactDOM.render() (before React 17) | createRoot() (React 18+) |
|---|---|---|
| Rendering type | Synchronous (legacy) - React blocks the JS thread until it finishes rendering | Concurrent - React can pause, resume, and order renders by priority |
| Reaction to updates | All updates run sequentially, with no priorities | Updates are split into urgent / non-urgent (via startTransition, useDeferredValue, etc.) |
| Automatic batching (grouping setState) | Works only inside React events | Works everywhere - even in setTimeout, promises, and async functions |
| Support for new React 18+ features | No (concurrent capabilities can't be used) | Yes (includes everything: concurrent rendering, Suspense SSR, streaming, etc.) |
| SSR API | ReactDOM.hydrate() | ReactDOM.hydrateRoot() |
| Update control (flush, transitions) | No control | Has useTransition, startTransition, flushSync hooks |
2. Example before and after React 18
Before (React 17)
import ReactDOM from "react-dom";
import App from "./App";
ReactDOM.render(<App />, document.getElementById("root"));This call uses the old synchronous mechanism. If the app runs a heavy render, React can't interrupt it, and the UI may "freeze".
After (React 18+)
import { createRoot } from "react-dom/client";
import App from "./App";
const container = document.getElementById("root");
const root = createRoot(container); // a new "root"
root.render(<App />);This approach creates a Concurrent Root. Now React can:
- pause rendering;
- defer "low-priority" updates;
- perform automatic batching;
- work with
startTransition()anduseDeferredValue().
3. Behavior when updating state
An example with asynchronous code:
React 17 (the old render)
setTimeout(() => {
setCount(c => c + 1);
setFlag(f => !f);
// there will be 2 separate renders
}, 1000);React 18 (createRoot)
setTimeout(() => {
setCount(c => c + 1);
setFlag(f => !f);
// one combined render thanks to Automatic Batching
}, 1000);React now merges updates itself even outside React events (for example, in promises or timers).
4. Impact on hooks and new capabilities
createRoot() includes support for new React 18 APIs:
useTransition()/startTransition()- controlling update priorityuseDeferredValue()- deferred valuesuseId()- stable IDs between the client and the serverSuspensefor data fetching and SSR (streaming,renderToPipeableStream)
If you use ReactDOM.render(), these features don't work - React stays in "Legacy Mode".
5. Compatibility and migration
-
The old
ReactDOM.render()still works, but is marked as deprecated. -
Migration is extremely simple - just replace one call:
javascript- ReactDOM.render(<App />, root); + const root = createRoot(root); + root.render(<App />); -
The rest of the code (components, hooks, context, etc.) doesn't need to change.
Summary comparison in one sentence
ReactDOM.render()is the old synchronous render with no priorities.
createRoot()is the new concurrent render, which includes React 18's capabilities: batching, transitions, streaming SSR, and a smooth UI.
Short Answer
Interview readyA concise answer to help you respond confidently on this topic during an interview.