Skip to main content

How createRoot works

When following a React tutorial or the docs, the first step that you should do in order to use React is to import createRoot from react-dom/client, then call it by providing a container, then call the render function from the resulting root object.

import { App } from "./app";
import { createRoot } from "react-dom/client";

const container = document.getElementById("root");

// This is the first step
const root = createRoot(container);

// Then, the second
root.render(<App />);

This section is all about createRoot (the first step). We will see its signature, the purpose of creating the root object and what exactly is.

Signature

createRoot is defined as follows, you can see it here too:

function createRoot(
container: Element | Document | DocumentFragment,
options?: CreateRootOptions
): RootType { /* [Not Native code] */ }

createRoot accepts a DOM Node and returns an object of type RootType (The dom node is often called the HostRoot) that you will use to render your application. We will see the returned object in details later in this section.

The second optional argument of createRoot is an options object. Up until writing these words, here is the following supported options:

PropertyTypeDescription
unstable_strictModebooleanEnable/disable StrictMode at root level
unstable_concurrentUpdatesByDefaultbooleanMake concurrent updates the default for a root.
unstable_transitionCallbacksTransitionTracingCallbacksI don't know what are these. It will be documented/edited when we get to it.
identifierPrefixstringReact Flight root's identifierPrefix.
onRecoverableError(error: any) => voidCallback when React auto recovers from errors. Try it here.

TransitionTracingCallbacks are defined here.

note

Having the unstable_ prefix means that this option is still in development or experimental. Once stable and documented in React docs themselves, the unstable_ prefix will be removed and there are chances that even the name may change.

So, avoid experimental and unstable APIs unless you are fully confident that you can manage it (don't forget to leave some comments too to instruct the next developer 😉).

The root object is used by React to render you whole application and manage it over time. So it has a crucial role in your application: it is its Root. It has enough information that allows to know the state of your tree at any point in time and even manipulate it.

Implementation

TL;DR

if (!isValidContainer(container)) {
throw new Error('createRoot(...): Target container is not a DOM element.');
}
let isStrictMode = false;
let identifierPrefix = '';
// ...other options

if (options) {
if (options.unstable_strictMode === true) {
isStrictMode = true;
}
// ...
}
const fiberRoot = createContainer(
container, // the host element
ConcurrentRoot, // the root type, or RootTag
null, // hydration callbacks
isStrictMode, // options?.unstable_strictMode || false
isConcurrentUpdatesByDefault, // options?.unstable_concurrentUpdatesByDefault || false
identifierPrefix, // options?.identifierPrefix || ''
onRecoverableError, // options?.onRecoverableError || reportError || console.error
transitionCallbacks, // options?.unstable_transitionCallbacks || null
);
// Mark container as root
container.__reactContainer$randomValue = fiberRoot.current;
// Injet ReactDom dispatcher
Dispatcher.current = ReactDOMClientDispatcher;
return new ReactDOMRoot(fiberRoot);

1. Ensure that container is a valid React container

if (!isValidContainer(container)) {
throw new Error('createRoot(...): Target container is not a DOM element.');
}

Valid containers are:

2. Warn in dev about bad containers

In development builds, you may be warned if you violate one of the following:

  • using body as a container, which is often used by extensions and third party libraries, so it may fool React into reconciliation issues.

  • You previously called the legacy ReactDOM.render(container, element) on that container.

  • You already called createRoot with the same container.

    It is important to keep these things in mind and avoid them.

3. Close over the provided options

Next, we will declare variables mirroring the provided options and fall back to their default values.

// simplified
let isStrictMode = false;
let identifierPrefix = '';
// ...other options

if (options) {
if (options.unstable_strictMode === true) {
isStrictMode = true;
}
// ...
}

4. Call createContainer with the information in scope:

Now, the root actual creation:

const fiberRoot = createContainer(
container, // the host element
ConcurrentRoot, // the root type, or RootTag
null, // hydration callbacks
isStrictMode, // options?.unstable_strictMode || false
isConcurrentUpdatesByDefault, // options?.unstable_concurrentUpdatesByDefault || false
identifierPrefix, // options?.identifierPrefix || ''
onRecoverableError, // options?.onRecoverableError || reportError || console.error
transitionCallbacks, // options?.unstable_transitionCallbacks || null
);

The resulting object has many properties, for the sake of clarity of this particular section, we will skip over them until later. But we will see the creation sequence. createContainer itself will delegate the work to createFiberRoot with almost the same parameters, with a null value for the initialChildren and false for hydrate (obviously).

Now that we are at the real deal, let's break it step by step:

  1. Create an instance of a FiberRootNode

    const fiberRoot = new FiberRootNode(
    container, // the host element
    tag, // ConcurrentRoot
    hydrate, // false for this path
    identifierPrefix, // options?.identifierPrefix || ''
    onRecoverableError, // options?.onRecoverableError || reportError || console.error
    );

    This creation involves like mentioned many properties, don't worry, you will have a table later describing each one of them. But it is important that you sneak peek 😉.

  2. Create the first instance Fiber of kind HostRoot:

    By no doubts you've heard of the famous Fiber architecture in React, at this point, the first one is created.

    One important thing to detect is the React Mode, React will use it to decide which logic to perform in many cases.

    // simplified
    const unitializedFiber = new FiberNode(
    HostRoot, // tag
    null, // pendingProps
    null, // key
    mode, // deduced react mode (strict mode, strict effects, concurrent updates..)
    );

    We've skipped until now two major and important creations: FiberRootNode and FiberNode. We will see them in a few, but it is important that your mental model start grasping that, when creating a root for React, we create a special instance of FiberRootNode that will also have an attached FiberNode to it.

  3. Reference FiberNode and FiberRootNode in each other:

    fiberRoot.current = unitializedFiber;
    unitializedFiber.stateNode = fiberRoot;
  4. Initialize the FiberNode's memoizedState:

    This initialization is conditional as it changes a bit when the cache feature is enabled in React.

    // simplified
    uninitializedFiber.memoizedState = {
    element: null, // initialChildren
    isDehydrated: false, // hydrate
    cache: null, // put behind a feature flag
    }
  5. Initialize the FiberNode's updateQueue: This initialization creates the updateQueue property for our unintializedFiber:

    unitializedFiber.updateQueue = {
    baseState: fiber.memoizedState, // we just created this above
    firstBaseUpdate: null,
    lastBaseUpdate: null,
    shared: {
    pending: null,
    lanes: NoLanes, // 0
    hiddenCallbacks: null,
    },
    callbacks: null,
    };

    Don't worry, we will explain for what every one of them is used when time comes.

  6. Finally, return the FiberRootNode:

    return fiberRoot;

5. Mark the container as Root

Here, React will mutate your provided container object by adding a special property with a name unique for the loaded React instance.

// simplified
container.__reactContainer$randomValue = fiberRoot.current; // unintializedFiber

6. Inject the current ReactDispatcher

The Dispatcher concept in React will have its own section as it has so many gotchas.

At this point, we attach the ReactDOMClientDispatcher which is defined here.

We will come back to this in a later blog in order to fully explain it too.

There are several dispatchers used by React, they will all be explained.

The dispatcher set at this point is used in the server by ReactFloat. We'll get back to this in the right time. But for now, we won't be seeing it during simple client render.

Dispatcher.current = ReactDOMClientDispatcher;

7. Listen to all supported events on the provided container

As you may be aware of, React implemented a plugin event system that's detailed in its own section.

At this point, React will attach necessary event handlers to the root container with different priorities.

You can sneak peek starting from here before reading this section later.

8. Return an instance of type ReactDOMRoot

So this is the final step in createRoot!

This step only calls the ReactDOMRoot constructor with the resulting fiberRoot object.

The constructor itself only references the given fiberRoot into _internalRoot, that you may already have seen if you ever inspected the root object. But there are two methods too: render method and the unmount method.

function ReactDOMRoot(internalRoot: FiberRoot) {
this._internalRoot = internalRoot;
}

ReactDOMRoot.prototype.render = ... /* [Not Native Code] */
ReactDOMRoot.prototype.unmout = ... /* [Not Native Code] */
danger

The _internalRoot property is not documented in the React docs and should not be used.

Recap

Here is a small diagram illustrating the created objects with some of their properties (unstable and dev properties were omitted for now):

createRoot fingerprint

Annex

You can have a look at the details of the skipped objects properties if you want.

Details of FiberRootNode and FiberNode properties
FiberRootNode properties
PropertyTypeDescription
tagnumberConcurrentRoot or LegacyRoot, the type of the root.
containerInfoElementThe container passed to createRoot
pendingChildrenanyTBD
currentFiberNodeThe current Fiber instance for this root
pingCacheWeakMap<Wakeable, Set<mixed>>A cache around promises and ping listeners
finishedWorkFiber or nullA finished work in progress HostRoot ready to be committed
timeoutHandleTimeoutID or -1The ID of the timeout (host specific) for scheduling a fallback commit when tree is suspending
cancelPendingCommitnull or () => voidCancels the scheduled timeout for committing a suspending tree
contextObject or nullTBD
pendingContextObject or nullTBD
nextFiberRoot or nullCreates a linkedList of roots with pending work
callbackNodeanyTBD
callbackPriorityLaneTBD
expirationTimesLaneMap<number>TBD
hiddenUpdatesLaneMap<Array<ConcurrentUpdate> or null>TBD
pendingLanesLanesTBD
suspendedLanesLanesTBD
pingedLanesLanesTBD
expiredLanesLanesTBD
finishedLanesLanesTBD
errorRecoveryDisabledLanesLanesTBD
shellSuspendCounternumberTBD
entangledLanesLanesTBD
entanglementsLaneMap<Lanes>TBD
identifierPrefixstringTBD
onRecoverableError(error: mixed, errorInfo: {digest?: string, componentStack?: string}) => voidTBD
FiberNode properties
PropertyTypeDescription
tagWorkTag (number)The tag identifying the type of the fiber
keynull or stringThe unique identifier of this fiber
elementTypeReactElement.typeThe preserved element.type from your element
typeanyThe resolved function or class linked to this fiber
stateNodeanyTBD
returnFiber or nullThe parent fiber (almost)
childFiber or nullThe first child of this fiber (the tree)
siblingFiber or nullThis fiber's sibling
indexnumberThe index of this fiber, when a member of a list
refRefObjectTBD
refCleanupnull or () => voidTBD
pendingPropsanyWork in progress props
memoizedPropsanyCommitted props
updateQueueUpdateQueueA linkedList of pending updates on this fiber
memoizedStateanyTBD
dependenciesDependencies or nullTBD
modeTypeOfMode (number)A number describing the fiber properties and its subtree
flagsFlags (number)A number describing the behavior and capabilities of this fiber
subtreeFlagsFlags (number)The merged Flags from the children of this Fiber
deletionsArray<Fiber> or nullDeleted children fibers
nextEffectFiber or nullTBD
firstEffectFiber or nullTBD
lastEffectFiber or nullTBD
lanesLanesTBD
childLanesLanesTBD
alternateFiber or nullTBD

It may be not very readable or beneficial, but here is a codesandbox showing these properties from the created root object.

danger

During my time working with React, I NEVER used the root object or any of its properties.

The React team also discourage using them, because you will likely break React when manipulating them, and they change over time, so they aren't stable and using them should not be an option.

By now, we have created our FiberRoot object that will allow us to render our application in the given dom container.

In the next section we will read about how root.render() works.