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:
Property | Type | Description |
---|---|---|
unstable_strictMode | boolean | Enable/disable StrictMode at root level |
unstable_concurrentUpdatesByDefault | boolean | Make concurrent updates the default for a root. |
unstable_transitionCallbacks | TransitionTracingCallbacks | I don't know what are these. It will be documented/edited when we get to it. |
identifierPrefix | string | React Flight root's identifierPrefix. |
onRecoverableError | (error: any) => void | Callback when React auto recovers from errors. Try it here. |
TransitionTracingCallbacks
are defined here.
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:
- Dom elements
such as
div
,p
and so on. - The main page's Document
- Document Fragments
- Comments in React builds that have this feature allowed.
2. Warn in dev about bad containers
In development builds, you may be warned if you violate one of the following:
using
body
as acontainer
, 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 thatcontainer
.You already called
createRoot
with the samecontainer
.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:
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 😉.
Create the first instance
Fiber
of kindHostRoot
: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
andFiberNode
. We will see them in a few, but it is important that your mental model start grasping that, when creating aroot
for React, we create a special instance ofFiberRootNode
that will also have an attachedFiberNode
to it.Reference
FiberNode
andFiberRootNode
in each other:fiberRoot.current = unitializedFiber;
unitializedFiber.stateNode = fiberRoot;Initialize the
FiberNode
'smemoizedState
: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
}Initialize the
FiberNode
'supdateQueue
: This initialization creates theupdateQueue
property for ourunintializedFiber
: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.
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] */
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):
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
Property | Type | Description |
---|---|---|
tag | number | ConcurrentRoot or LegacyRoot , the type of the root. |
containerInfo | Element | The container passed to createRoot |
pendingChildren | any | TBD |
current | FiberNode | The current Fiber instance for this root |
pingCache | WeakMap<Wakeable, Set<mixed>> | A cache around promises and ping listeners |
finishedWork | Fiber or null | A finished work in progress HostRoot ready to be committed |
timeoutHandle | TimeoutID or -1 | The ID of the timeout (host specific) for scheduling a fallback commit when tree is suspending |
cancelPendingCommit | null or () => void | Cancels the scheduled timeout for committing a suspending tree |
context | Object or null | TBD |
pendingContext | Object or null | TBD |
next | FiberRoot or null | Creates a linkedList of roots with pending work |
callbackNode | any | TBD |
callbackPriority | Lane | TBD |
expirationTimes | LaneMap<number> | TBD |
hiddenUpdates | LaneMap<Array<ConcurrentUpdate> or null> | TBD |
pendingLanes | Lanes | TBD |
suspendedLanes | Lanes | TBD |
pingedLanes | Lanes | TBD |
expiredLanes | Lanes | TBD |
finishedLanes | Lanes | TBD |
errorRecoveryDisabledLanes | Lanes | TBD |
shellSuspendCounter | number | TBD |
entangledLanes | Lanes | TBD |
entanglements | LaneMap<Lanes> | TBD |
identifierPrefix | string | TBD |
onRecoverableError | (error: mixed, errorInfo: {digest?: string, componentStack?: string}) => void | TBD |
FiberNode properties
Property | Type | Description |
---|---|---|
tag | WorkTag (number) | The tag identifying the type of the fiber |
key | null or string | The unique identifier of this fiber |
elementType | ReactElement.type | The preserved element.type from your element |
type | any | The resolved function or class linked to this fiber |
stateNode | any | TBD |
return | Fiber or null | The parent fiber (almost) |
child | Fiber or null | The first child of this fiber (the tree) |
sibling | Fiber or null | This fiber's sibling |
index | number | The index of this fiber, when a member of a list |
ref | RefObject | TBD |
refCleanup | null or () => void | TBD |
pendingProps | any | Work in progress props |
memoizedProps | any | Committed props |
updateQueue | UpdateQueue | A linkedList of pending updates on this fiber |
memoizedState | any | TBD |
dependencies | Dependencies or null | TBD |
mode | TypeOfMode (number) | A number describing the fiber properties and its subtree |
flags | Flags (number) | A number describing the behavior and capabilities of this fiber |
subtreeFlags | Flags (number) | The merged Flags from the children of this Fiber |
deletions | Array<Fiber> or null | Deleted children fibers |
nextEffect | Fiber or null | TBD |
firstEffect | Fiber or null | TBD |
lastEffect | Fiber or null | TBD |
lanes | Lanes | TBD |
childLanes | Lanes | TBD |
alternate | Fiber or null | TBD |
It may be not very readable or beneficial, but here is
a codesandbox showing these properties from the created root
object.
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.