What is ReactDOM.createRoot and how is it different from ReactDOM.render?
React 18 introduces a new root API, so let's figure it out
π Current API
We have to pass the container to render function every time we want to explicitly render.
const container = document.querySelector('#root');
// Initial render. Container is explicitly accessed.
ReactDOM.render(<App text="Hello" />, container);
// Subsequent renders. Container is explicitly accessed.
ReactDOM.render(<App text="Hello world!" />, container);
π What does ReactDOM.render take?
render function takes three arguments:
- React element to be rendered
 - DOM element to render in
 - function to be executed after render happens
 
And returns the same container, but with the rendered component.
/**
* @param element - React element to be rendered
* @param container - DOM element to render in
* @param callback - function to be executed after render happens
* @return container - container with renderned component
*/
function render(element, container, callback) {
  // ...
}
π How does ReactDOM.render work under the hood?
ReactDOM.render does a couple of validation checks:
- whether the container is a suitable node
 - whether the container wasn't previously passed to 
createRoot 
Then it passes all received arguments to legacyRenderSubtreeIntoContainer.
// simplified structure
function render(element, container, callback) {
  if (isValidContainer(element)) {
    throw Error('Target container is not a DOM element.');
  }
  if (isContainerMarkedAsRoot(container) && container._reactRootContainer === undefined) {
    // don't throw an error, but logs it into console
    error('container was previously passed to ReactDOM.createRoot().');
  }
  return legacyRenderSubtreeIntoContainer(null, element, container, false, callback);
}
π New API
It fixes the issue of passing the container every time we want to explicitly render.
// First, we create a root
const root = ReactDOM.createRoot(document.querySelector('#root'));
// Initial render. Container is implicitly accessed.
root.render(<App name="Hello" />);
// Subsequent renders. Container is implicitly accessed.
root.render(<App name="Hello world!" />);
π What does ReactDOM.createRoot take?
createRoot function takes only one mandatory argument - DOM element to render in.
And returns RootType, which has render and unmount methods.
P.S. Also createRoot takes the second RootOptions argument, but we'll examine it in the future.
/**
* @param container - DOM element to render in
* @param options - options, related to hydration
* @return RootType - instance of root
*/
function createRoot(container, options) {
  // ...
}
π How does ReactDOM.createRoot work under the hood?
On top of the render function, createRoot:
- checks whether the container isn't a body element
 - provide a bit more detailed warnings
 
Then createRoot instance a new ReactDOMRoot object and returns it. No legacyRenderSubtreeIntoContainer at all!
// simplified structure
function createRoot(container, options) {
  if (isValidContainer(element)) {
    throw Error('Target container is not a DOM element.');
  }
  if (container.nodeType === 1 && container.tagName.toUpperCase() === 'BODY') {
    console.error('Creating roots directly with document.body is discouraged');
  }
  if (isContainerMarkedAsRoot(container) {
    if (container._reactRootContainer) {
      console.error('container was previously passed to ReactDOM.render().')
    } else {
      console.error('container has already been passed to createRoot() before.');
    }
  }
  return new ReactDOMRoot(container, options);
}
Now you know the underline difference between old render and new createRoot functions!
Let me know, if you want a further comparison of legacyRenderSubtreeIntoContainer and new ReactDOMRoot.
P.S. Follow me on Twitter for more content like this!
              
    
Top comments (2)
So useful!
Thanks ππ»
I appreciate this kind of feedback a lot!