Skip to main content

createRoot() vs render()

1. The main difference

FeatureReactDOM.render() (before React 17)createRoot() (React 18+)
Rendering typeSynchronous (legacy) - React blocks the JS thread until it finishes renderingConcurrent - React can pause, resume, and order renders by priority
Reaction to updatesAll updates run sequentially, with no prioritiesUpdates are split into urgent / non-urgent (via startTransition, useDeferredValue, etc.)
Automatic batching (grouping setState)Works only inside React eventsWorks everywhere - even in setTimeout, promises, and async functions
Support for new React 18+ featuresNo (concurrent capabilities can't be used)Yes (includes everything: concurrent rendering, Suspense SSR, streaming, etc.)
SSR APIReactDOM.hydrate()ReactDOM.hydrateRoot()
Update control (flush, transitions)No controlHas useTransition, startTransition, flushSync hooks

2. Example before and after React 18

Before (React 17)

javascript
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+)

javascript
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() and useDeferredValue().

3. Behavior when updating state

An example with asynchronous code:

React 17 (the old render)

javascript
setTimeout(() => { setCount(c => c + 1); setFlag(f => !f); // there will be 2 separate renders }, 1000);

React 18 (createRoot)

javascript
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 priority
  • useDeferredValue() - deferred values
  • useId() - stable IDs between the client and the server
  • Suspense for 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 ready
Premium

A concise answer to help you respond confidently on this topic during an interview.