createRoot in React 18: The Complete Migration Guide to Concurrent Rendering
createRoot is the mandatory entry point for React 18's Concurrent rendering pipeline that creates a persistent root object to manage the internal Fiber tree, enabling time-slicing, Suspense boundaries, and interruptible rendering while replacing the legacy ReactDOM.render() API.
When migrating a React application from the legacy ReactDOM API to modern concurrent features, understanding createRoot is essential. According to the facebook/react source code, this function fundamentally rearchitects how React attaches to the DOM by establishing a persistent container in packages/react-dom/src/client/ReactDOMRoot.js that owns its own scheduling lifecycle and rendering state.
What createRoot Does in React 18
Unlike the legacy ReactDOM.render() which immediately mounted a component, createRoot establishes a persistent root object (ReactDOMRoot) that acts as the gateway to React's concurrent capabilities. This object maintains the Fiber tree, manages rendering priorities, and provides the render() and unmount() methods for lifecycle control.
The function performs six critical operations defined in the React source:
- Validates the DOM container using
isValidContainerto ensure the target is a proper DOM element (packages/react-dom/src/client/ReactDOMRoot.jslines 71-78) - Configures Concurrent Mode by setting the
ConcurrentRootflag, which enables time-slicing and interruptible rendering (lines 98-101) - Initializes the Fiber root via
createContainer, establishing the internal tree structure with options for strict mode and error handling (lines 136-149) - Marks the container using
markContainerAsRootto prevent accidental double-mounting and identify React-managed nodes (lines 149-151) - Wires the event system through
listenToAllSupportedEvents, attaching React's synthetic event delegation to the container or its parent (lines 253-256) - Returns the Root object exposing
root.render()to schedule updates viaupdateContainer(lines 105-111) androot.unmount()to cleanup viaupdateContainerSyncandflushSyncWork(lines 138-146)
Migrating from ReactDOM.render to createRoot
The migration requires changing from the imperative legacy API to the new root-based pattern. Instead of calling ReactDOM.render directly, you create a root once and call render on that object.
Replace your legacy entry point:
import ReactDOM from 'react-dom';
import App from './App';
// Legacy API - immediately renders
ReactDOM.render(<App />, document.getElementById('root'));
With the modern concurrent approach:
import { createRoot } from 'react-dom/client';
import App from './App';
const container = document.getElementById('root');
// Create a persistent concurrent root
const root = createRoot(container);
// Schedule the initial render
root.render(<App />);
Configuring Error Boundaries and Strict Mode
The createRoot function accepts an options object as its second parameter, allowing configuration of error handling and development behaviors. These options are passed through to createContainer during Fiber root initialization.
import { createRoot } from 'react-dom/client';
const root = createRoot(document.getElementById('root'), {
// Enable new Strict Mode behaviors for concurrent features
unstable_strictMode: true,
// Handle uncaught errors from the Fiber tree
onUncaughtError: (error, errorInfo) => {
console.error('Uncaught React error:', error, errorInfo.componentStack);
},
// Track transition states
onDefaultTransitionIndicator: (isPending) => {
console.log('Transition status:', isPending ? 'pending' : 'complete');
}
});
root.render(<App />);
Hydrating Server-Rendered Markup
For server-side rendered applications, use hydrateRoot instead of createRoot. This companion function creates a concurrent root while hydrating existing HTML markup, maintaining the same internal architecture but attaching to pre-rendered content.
import { hydrateRoot } from 'react-dom/client';
import App from './App';
const container = document.getElementById('root');
// Creates concurrent root while hydrating SSR markup
const root = hydrateRoot(container, <App />, {
unstable_strictMode: true
});
// No need to call root.render() - hydration occurs immediately
Summary
createRootreplacesReactDOM.renderas the entry point for React 18 applications, returning a persistentReactDOMRootobject rather than mounting immediately.- Always creates a ConcurrentRoot, enabling time-slicing, Suspense, and interruptible rendering regardless of whether you use concurrent features explicitly.
- Validates and marks containers to prevent double-mounting and ensure DOM element compatibility through
isValidContainerandmarkContainerAsRoot. - Initializes the Fiber tree via
createContainerinpackages/react-dom/src/client/ReactDOMRoot.js, configuring error boundaries and strict mode options. - Exposes
render()andunmount()methods that schedule updates through the concurrent reconciler rather than synchronous rendering. - Wires React's event system automatically via
listenToAllSupportedEvents, setting up synthetic event delegation on the container.
Frequently Asked Questions
What happens if I continue using ReactDOM.render in React 18?
React 18 will operate in legacy mode, disabling all concurrent features including automatic batching, Suspense improvements, and transitions. The console will display a deprecation warning urging migration to createRoot, and you will not benefit from the performance optimizations available in the new rendering pipeline.
Can I use createRoot with React 17 or earlier?
No, createRoot is exclusively available in React 18 and later. Attempting to import it from react-dom/client in earlier versions will throw a module not found error. The ConcurrentRoot flag and Fiber architecture changes required for createRoot functionality were introduced specifically in the React 18 release.
What is the difference between createRoot and hydrateRoot?
While both create concurrent roots, createRoot is for client-side rendered applications calling root.render() to mount to an empty DOM container, whereas hydrateRoot is for server-side rendered applications where HTML already exists. hydrateRoot accepts the initial React element as its second argument and performs hydration immediately without requiring a separate render() call.
How do I properly unmount a React 18 application?
Call the unmount() method on the root object returned by createRoot. This triggers updateContainerSync and flushSyncWork to cleanly destroy the Fiber tree, detach event listeners, and unmark the container. Unlike the legacy ReactDOM.unmountComponentAtNode(), this method is instance-specific and handles cleanup through the concurrent scheduler.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →