Let’s dive straight into React’s initialization process today. Understanding this process helps us see how React maps JSX step-by-step to the interface, which is essential. As we know, in React v18, we generally render our root component App
using the following approach:
// Step 1: Create root const root = ReactDOM.createRoot(document.getElementById("root")); // Step 2: Render root.render(<App />);
This process consists of two main steps:
Creating the root node (
root
).Calling the
render
method to render the component.
This article aims to simplify and analyze how this process ultimately renders on the page. By the end, you will have answers to questions like:
What’s the difference between
fiberRoot
androotFiber
?What is event priority?
What does the structure of a fiber look like?
Where are React’s events registered?
How does React address the performance issues caused by too many events?
And more…
Note: This article simplifies the core elements from the source code. For detailed specifics, visit the React repository.
Pre-Preparation Phase
Before rendering, React 18 goes through a pre-preparation phase, mainly to create a root node, which starts with ReactDOM.createRoot
. Let’s break it down:
function createRoot(container, options) { // ...some validations var isStrictMode = false; var concurrentUpdatesByDefaultOverride = false; var identifierPrefix = ""; var onRecoverableError = defaultOnRecoverableError; var transitionCallbacks = null; // ...options parameter, generally not used // Create root var root = createContainer( container, ConcurrentRoot, // 1 null, isStrictMode, concurrentUpdatesByDefaultOverride, identifierPrefix, onRecoverableError ); listenToAllSupportedEvents(container); return new ReactDOMRoot(root); // Wrap the root }
The entry function is straightforward and mainly does two things:
Creates the root node.
Sets up event listeners.
When we create the root using createRoot()
, it’s tagged to indicate that the app is in Concurrent mode, guiding the later reconciliation process. There are actually two modes:
var LegacyRoot = 0; // ReactDOM.render(<App />, document.getElementById("#root")) var ConcurrentRoot = 1; // ReactDOM.createRoot(document.getElementById("#root")).render(<App />)
After initialization, all interactions are event-driven, so events need to be registered early. Now, let’s look at each step.
Creating the Root
We need to look into createContainer
in more detail:
// createContainer is simple; its purpose is to create a FiberRoot, which we call the root node function createContainer( containerInfo, tag, hydrationCallbacks, isStrictMode, concurrentUpdatesByDefaultOverride, identifierPrefix, onRecoverableError, transitionCallbacks ) { var hydrate = false; var initialChildren = null; return createFiberRoot( containerInfo, tag, hydrate, initialChildren, hydrationCallbacks, isStrictMode, concurrentUpdatesByDefaultOverride, identifierPrefix, onRecoverableError ); } // The above essentially calls createFiberRoot directly, so let’s look at what this function does function createFiberRoot( containerInfo, // This is the #root DOM tag, // Represents Concurrent mode, with a value of 1 // The following are mostly false or null, not used right now hydrate, initialChildren, hydrationCallbacks, isStrictMode, concurrentUpdatesByDefaultOverride, identifierPrefix, onRecoverableError, transitionCallbacks ) { // Instantiate a new FiberRootNode var root = new FiberRootNode( containerInfo, tag, hydrate, identifierPrefix, onRecoverableError ); // Create a Fiber node representing the root Fiber node of type Fiber var uninitializedFiber = createHostRootFiber(tag, isStrictMode); // Link them with mutual references root.current = uninitializedFiber; uninitializedFiber.stateNode = root; // Create an initial state and assign it to the first Fiber node { var _initialState = { element: initialChildren, isDehydrated: hydrate, cache: null, // not enabled yet transitions: null, pendingSuspenseBoundaries: null, }; uninitializedFiber.memoizedState = _initialState; } // Initialize the updateQueue for the first Fiber node initializeUpdateQueue(uninitializedFiber); return root; }
This function performs several key tasks:
Creates the actual root node (
FiberRoot
).Creates the first Fiber node.
Links the nodes with mutual references.
Initializes the first Fiber’s
memoizedState
andupdateQueue
.
After this step, the structure in memory looks like this:
This overall process is relatively simple. It’s crucial to understand the two types involved: FiberRoot
and RootFiber
.
FiberRoot and RootFiber
FiberRoot is a type of root node, and in the runtime of a React application, there is only one instance of it. React relies on FiberRoot to switch between fiber trees for updates, making it a crucial component. Let's take a closer look at its implementation.
Code Analysis
function FiberRootNode( containerInfo, tag, hydrate, identifierPrefix, onRecoverableError ) { this.tag = tag; // LegacyRoot/ConcurrentRoot this.containerInfo = containerInfo; // root DOM node this.pendingChildren = null; this.current = null; // Fiber tree of the current view this.pingCache = null; this.finishedWork = null; // Committed fiber tree this.timeoutHandle = noTimeout; // Macro task timer this.context = null; this.pendingContext = null; // Value from getContextForSubtree(parentComponent) this.callbackNode = null; // Task type in the scheduler system this.callbackPriority = NoLane; // Priority in the scheduler system (1, 2, 3, 4, 5) this.eventTimes = createLaneMap(NoLanes); // Array of 32 timestamps representing events' trigger times this.expirationTimes = createLaneMap(NoTimestamp); // Array of 32 timestamps for expiration times, calculated based on lane and eventTime // Priority scheduling this.pendingLanes = NoLanes; this.suspendedLanes = NoLanes; this.pingedLanes = NoLanes; this.expiredLanes = NoLanes; this.mutableReadLanes = NoLanes; this.finishedLanes = NoLanes; // Commit render this.entangledLanes = NoLanes; this.entanglements = createLaneMap(NoLanes); this.identifierPrefix = identifierPrefix; this.onRecoverableError = onRecoverableError; { this.mutableSourceEagerHydrationData = null; } { this.effectDuration = 0; this.passiveEffectDuration = 0; } { this.memoizedUpdaters = new Set(); var pendingUpdatersLaneMap = (this.pendingUpdatersLaneMap = []); for (var _i = 0; _i < TotalLanes; _i++) { pendingUpdatersLaneMap.push(new Set()); } } ... }
As you can see, the only actual values assigned are the root DOM node (#root
) and the tag
identifier. The rest are various fields with specific functions, which will be covered in later sections.
Understanding React's Fiber Type
Many of you may already be familiar with React’s Fiber type, as it’s a central concept in React. RootFiber is the first Fiber node, and I will cover some basics of the Fiber type for completeness. Those who already know this well can skip ahead.
In React, Fiber is a unique data structure, not purely a tree or linked list but a hybrid of both. It’s integral to the initialization and update processes of React runtime. Fiber is foundational to features like hooks, update interruption, and priority management. Without Fiber, implementing these features would be exceedingly challenging.
There are 26 types of Fiber, categorized as follows:
export const FunctionComponent = 0; // Functional component export const ClassComponent = 1; // Class component export const IndeterminateComponent = 2; // Component type undecided (function or class) export const HostRoot = 3; // Root Fiber component of the app export const HostPortal = 4; // Entry point for a different renderer export const HostComponent = 5; // Native DOM component export const HostText = 6; // Native text component export const Fragment = 7; // Empty component // For reference only export const Mode = 8; export const ContextConsumer = 9; export const ContextProvider = 10; export const ForwardRef = 11; export const Profiler = 12; export const SuspenseComponent = 13; export const MemoComponent = 14; export const SimpleMemoComponent = 15; export const LazyComponent = 16; export const IncompleteClassComponent = 17; export const DehydratedFragment = 18; export const SuspenseListComponent = 19; export const ScopeComponent = 21; export const OffscreenComponent = 22; export const LegacyHiddenComponent = 23; export const CacheComponent = 24; export const TracingMarkerComponent = 25;
Every component we create in React has a corresponding fiber node. The tag
property on fiber nodes distinguishes their types.
Fiber nodes don’t hold significance individually; rather, their relationships form a tree structure, known as a fiber tree. For instance, given the following JSX:
const App = () => { return ( <div> <button></button> <p> <span>hello</span> <i></i> </p> <div> ); }
In memory, this creates a fiber tree like the following:
Each node in this tree has at most one child
pointer to its first child, one sibling
pointer to its next sibling, and a return
pointer to its parent, except for the root fiber node.
Now, returning to our main topic, the createHostRootFiber(tag, isStrictMode)
function creates the first fiber node:
function createHostRootFiber(tag, isStrictMode, concurrentUpdatesByDefaultOverride) { var mode; // Mode is set to 1, as we are using createRoot if (tag === ConcurrentRoot) { mode = ConcurrentMode; if (isStrictMode === true) { mode |= StrictLegacyMode; mode |= StrictEffectsMode; } } else { mode = NoMode; } return createFiber(HostRoot, null, null, mode); } // createFiber var createFiber = function (tag, pendingProps, key, mode) { return new FiberNode(tag, pendingProps, key, mode); // Creates a new fiber instance }; // FiberNode function function FiberNode(tag, pendingProps, key, mode) { this.tag = tag; // Fiber type (26 types) this.key = key; this.elementType = null; this.type = null; this.stateNode = null; this.return = null; this.child = null; this.sibling = null; this.index = 0; this.ref = null; this.pendingProps = pendingProps; this.memoizedProps = null; this.updateQueue = null; this.memoizedState = null; this.dependencies = null; this.mode = mode; this.flags = NoFlags; this.subtreeFlags = NoFlags; this.deletions = null; this.lanes = NoLanes; this.childLanes = NoLanes; this.alternate = null; ... }
After initialization, the first fiber node lacks a return
pointer, making it unique in the fiber tree.
Summary
We’ve examined the differences between the first fiber node, RootFiber, and the root of the application, FiberRoot. These two are interlinked and will be explored further in future articles.
Event Registration
To enhance performance, React uses event delegation. If you want an in-depth understanding of event delegation, check out this article.
In simple terms, event delegation means that, in real-world scenarios, users are likely to register various events on numerous nodes to monitor user actions. React handles all interactions by listening on the #root
node. By registering only a few events on #root
, it avoids having to register thousands of events on individual nodes, significantly improving performance.
This is handled by the function listenToAllSupportedEvents(container);
. In React 18, all events are delegated at the #root
node. Let’s look at what happens within this function:
function listenToAllSupportedEvents(rootContainerElement) { if (!rootContainerElement[listeningMarker]) { // Prevents duplicate registration rootContainerElement[listeningMarker] = true; // Adds a marker allNativeEvents.forEach(function (domEventName) { if (domEventName !== "selectionchange") { // Filters out 'selectionchange' as it doesn't bubble and can only be listened to at the Document level if (!nonDelegatedEvents.has(domEventName)) { listenToNativeEvent(domEventName, false, rootContainerElement); } listenToNativeEvent(domEventName, true, rootContainerElement); } }); var ownerDocument = rootContainerElement.nodeType === DOCUMENT_NODE ? rootContainerElement : rootContainerElement.ownerDocument; if (ownerDocument !== null && !ownerDocument[listeningMarker]) { ownerDocument[listeningMarker] = true; listenToNativeEvent("selectionchange", false, ownerDocument); } } }
At this point, React registers nearly all browser events (a total of 82), as shown in the image below.
Among these events, three categories exist: selectionchange
, events that don't support bubbling, and regular events. They are each handled separately, as you can see in the code. Each event type has unique characteristics. For instance, selectionchange
has no effect on regular DOM elements, and events like cancel
and abort
don't bubble. Refer to the MDN documentation for more information on events.
To closely simulate the native event flow, React listens at both the capture and bubble phases. This is an improvement in v18, as earlier versions used an array to simulate bubbling and capturing, which couldn't fully align with the native event flow.
To understand which function is registered, we can examine listenToNativeEvent
. Assuming the event is click
, let’s focus on the core part:
function listenToNativeEvent(domEventName, isCapturePhaseListener, target) { var eventSystemFlags = 0; if (isCapturePhaseListener) { eventSystemFlags |= IS_CAPTURE_PHASE; // Constant 4 } addTrappedEventListener( target, // #root domEventName, // e.g., 'click' eventSystemFlags, // 4 for capture, 0 for bubble isCapturePhaseListener // Capture phase flag ); }
The addTrappedEventListener
function does two things:
It creates a specific event listener function based on the event type.
It registers this function on
#root
.
Here’s how listener
is created:
function createEventListenerWrapperWithPriority(targetContainer, domEventName, eventSystemFlags) { var eventPriority = getEventPriority(domEventName); var listenerWrapper; switch (eventPriority) { case DiscreteEventPriority: listenerWrapper = dispatchDiscreteEvent; break; case ContinuousEventPriority: listenerWrapper = dispatchContinuousEvent; break; case DefaultEventPriority: default: listenerWrapper = dispatchEvent; break; } return listenerWrapper.bind(null, domEventName, eventSystemFlags, targetContainer); }
Here, we introduce the concept of event priority. For an optimal user experience, each event is assigned a priority level to determine its scheduling order. For example, in a text input event, users expect an immediate response; otherwise, it feels laggy. Events like scroll
, which trigger continuously, have a lower priority. This priority is set using the getEventPriority
function.
If the event is click
, dispatchDiscreteEvent
is returned, and it is registered on #root
as follows:
function addEventBubbleListener(target, eventType, listener) { target.addEventListener(eventType, listener, false); return listener; }
This code simply adds the event listener, completing the registration.
Summary
During createRoot
, RootFiber
and FiberRoot
are created, and all browser events are registered on #root
. When these events are triggered, a listener is called (e.g., for click
, dispatchDiscreteEvent
is invoked).
However, the initialization process is not fully complete here. The instance exposed to the user is actually of type ReactDOMRoot
:
function createRoot() { return new ReactDOMRoot(root); } function ReactDOMRoot(internalRoot) { this._internalRoot = internalRoot; } ReactDOMRoot.prototype.render = function (children) { var root = this._internalRoot; updateContainer(children, root, null, null); };
This instance has a .render
method, allowing developers to call root.render(<App/>)
, marking the start of the render phase, which we’ll discuss in the next section.