How does React do the initial mount internally?

In Overview of React internals we briefly mentioned that React uses tree-like structure(Fiber Tree) internally to calculate the minimum DOM updates and commit them in Commit phase. In this post, we’ll figure out how exactly React does the initial mount (first-time render). To be more specific, we’d like to know how React construct the DOM from code below.

import {useState} from 'react'
function Link() {
  return <a href="https://jser.dev">jser.dev</a>
}
export default function App() {
  const [count, setCount] = useState(0)
  return (
    <div>
      <p>
        <Link/>
        <br/>
        <button onClick={() => setCount(count => count + 1)}>click me - {count}</button>
      </p>
    </div>
  );
}

1. Brief introduction on Fiber Architecture

Fiber is the architecture of how React holds internal representation of the App state. It is a tree-like structure consisting FiberRootNode and FiberNodes. There are all kinds of FiberNode and some of them have backing DOM node - HostComponent.

React runtime tries its best to maintain and update the Fiber Tree and sync the host DOM with minimum updates.

1.1 FiberRootNode

FiberRootNode is a special node that acts as the React root, it holds the necessary meta info about the whole app. Its current points to the actual Fiber Tree, every time a new Fiber Tree is constructed, it re-points its current to the new HostRoot.

1.2 FiberNode

FiberNode means all nodes other than FiberRootNode, some of important properties are:

  1. tag: FiberNode has many sub type differentiated by tag. For example, FunctionComponent, HostRoot, ContextConsumer,MemoComponent,SuspenseComponent .etc
  2. stateNode: it points to other backing data, for HostComponent, stateNode points to the actual backing DOM node.
  3. child, sibling and return : these together form a tree-like structure.
  4. elementType which is the component function or intrinsic HTML tag we provide.
  5. flags: to indicate the updates to apply in Commit phase. subtreeFlags is for its subtree.
  6. lanes: to indicate the priority of pending updates. childLanes if for its subtree.
  7. memoizedState: points to its important data, for FunctionComponent it means the hooks.

2. Initial mount in Trigger phase

createRoot() creates React root which also has a dummy HostRoot FiberNode created as current.

export function createRoot(
container: Element | Document | DocumentFragment,
options?: CreateRootOptions,
): RootType {
let isStrictMode = false;
let concurrentUpdatesByDefaultOverride = false;
let identifierPrefix = '';
let onRecoverableError = defaultOnRecoverableError;
let transitionCallbacks = null;
const root = createContainer(

This returns FiberRootNode

container,
ConcurrentRoot,
null,
isStrictMode,
concurrentUpdatesByDefaultOverride,
identifierPrefix,
onRecoverableError,
transitionCallbacks,
);
markContainerAsRoot(root.current, container);
Dispatcher.current = ReactDOMClientDispatcher;
const rootContainerElement: Document | Element | DocumentFragment =
container.nodeType === COMMENT_NODE
? (container.parentNode: any)
: container;
listenToAllSupportedEvents(rootContainerElement);
return new ReactDOMRoot(root);
}
export function createContainer(
containerInfo: Container,
tag: RootTag,
hydrationCallbacks: null | SuspenseHydrationCallbacks,
isStrictMode: boolean,
concurrentUpdatesByDefaultOverride: null | boolean,
identifierPrefix: string,
onRecoverableError: (error: mixed) => void,
transitionCallbacks: null | TransitionTracingCallbacks,
): OpaqueRoot {
const hydrate = false;
const initialChildren = null;
return createFiberRoot(
---------------
containerInfo,
tag,
hydrate,
initialChildren,
hydrationCallbacks,
isStrictMode,
concurrentUpdatesByDefaultOverride,
identifierPrefix,
onRecoverableError,
transitionCallbacks,
);
}
export function createFiberRoot(
containerInfo: Container,
tag: RootTag,
hydrate: boolean,
initialChildren: ReactNodeList,
hydrationCallbacks: null | SuspenseHydrationCallbacks,
isStrictMode: boolean,
concurrentUpdatesByDefaultOverride: null | boolean,
identifierPrefix: string,
onRecoverableError: null | ((error: mixed) => void),
transitionCallbacks: null | TransitionTracingCallbacks,
): FiberRoot {
// $FlowFixMe[invalid-constructor] Flow no longer supports calling new on functions
const root: FiberRoot = (new FiberRootNode(
-------------
containerInfo,
tag,
hydrate,
identifierPrefix,
onRecoverableError,
): any);
// Cyclic construction. This cheats the type system right now because
// stateNode is any.
const uninitializedFiber = createHostRootFiber(
tag,
isStrictMode,
concurrentUpdatesByDefaultOverride,
);
root.current = uninitializedFiber;

A FiberNode of HostRoot is created and assigned as current of React root

uninitializedFiber.stateNode = root;
...
initializeUpdateQueue(uninitializedFiber);
return root;
}
export function createRoot(
container: Element | Document | DocumentFragment,
options?: CreateRootOptions,
): RootType {
let isStrictMode = false;
let concurrentUpdatesByDefaultOverride = false;
let identifierPrefix = '';
let onRecoverableError = defaultOnRecoverableError;
let transitionCallbacks = null;
const root = createContainer(

This returns FiberRootNode

container,
ConcurrentRoot,
null,
isStrictMode,
concurrentUpdatesByDefaultOverride,
identifierPrefix,
onRecoverableError,
transitionCallbacks,
);
markContainerAsRoot(root.current, container);
Dispatcher.current = ReactDOMClientDispatcher;
const rootContainerElement: Document | Element | DocumentFragment =
container.nodeType === COMMENT_NODE
? (container.parentNode: any)
: container;
listenToAllSupportedEvents(rootContainerElement);
return new ReactDOMRoot(root);
}
export function createContainer(
containerInfo: Container,
tag: RootTag,
hydrationCallbacks: null | SuspenseHydrationCallbacks,
isStrictMode: boolean,
concurrentUpdatesByDefaultOverride: null | boolean,
identifierPrefix: string,
onRecoverableError: (error: mixed) => void,
transitionCallbacks: null | TransitionTracingCallbacks,
): OpaqueRoot {
const hydrate = false;
const initialChildren = null;
return createFiberRoot(
---------------
containerInfo,
tag,
hydrate,
initialChildren,
hydrationCallbacks,
isStrictMode,
concurrentUpdatesByDefaultOverride,
identifierPrefix,
onRecoverableError,
transitionCallbacks,
);
}
export function createFiberRoot(
containerInfo: Container,
tag: RootTag,
hydrate: boolean,
initialChildren: ReactNodeList,
hydrationCallbacks: null | SuspenseHydrationCallbacks,
isStrictMode: boolean,
concurrentUpdatesByDefaultOverride: null | boolean,
identifierPrefix: string,
onRecoverableError: null | ((error: mixed) => void),
transitionCallbacks: null | TransitionTracingCallbacks,
): FiberRoot {
// $FlowFixMe[invalid-constructor] Flow no longer supports calling new on functions
const root: FiberRoot = (new FiberRootNode(
-------------
containerInfo,
tag,
hydrate,
identifierPrefix,
onRecoverableError,
): any);
// Cyclic construction. This cheats the type system right now because
// stateNode is any.
const uninitializedFiber = createHostRootFiber(
tag,
isStrictMode,
concurrentUpdatesByDefaultOverride,
);
root.current = uninitializedFiber;

A FiberNode of HostRoot is created and assigned as current of React root

uninitializedFiber.stateNode = root;
...
initializeUpdateQueue(uninitializedFiber);
return root;
}

root.render() schedules update on HostRoot. The argument of element is stored in update payload.

function ReactDOMRoot(internalRoot: FiberRoot) {
this._internalRoot = internalRoot;
}
ReactDOMHydrationRoot.prototype.render = ReactDOMRoot.prototype.render =
function (children: ReactNodeList): void {
const root = this._internalRoot;
if (root === null) {
throw new Error('Cannot update an unmounted root.');
}
updateContainer(children, root, null, null);
};
function ReactDOMRoot(internalRoot: FiberRoot) {
this._internalRoot = internalRoot;
}
ReactDOMHydrationRoot.prototype.render = ReactDOMRoot.prototype.render =
function (children: ReactNodeList): void {
const root = this._internalRoot;
if (root === null) {
throw new Error('Cannot update an unmounted root.');
}
updateContainer(children, root, null, null);
};
export function updateContainer(
element: ReactNodeList,
container: OpaqueRoot,
parentComponent: ?React$Component<any, any>,
callback: ?Function,
): Lane {
const current = container.current;
const lane = requestUpdateLane(current);
if (enableSchedulingProfiler) {
markRenderScheduled(lane);
}
const context = getContextForSubtree(parentComponent);
if (container.context === null) {
container.context = context;
} else {
container.pendingContext = context;
}
const update = createUpdate(lane);
// Caution: React DevTools currently depends on this property
// being called "element".
update.payload = {element};

The argument of render() is stored in the update payload

const root = enqueueUpdate(current, update, lane);

Then the update is enqueued. We won't dive into how this is done

Just remember that the update is waiting to be processed.

if (root !== null) {
scheduleUpdateOnFiber(root, current, lane);
entangleTransitions(root, current, lane);
}
return lane;
}
export function updateContainer(
element: ReactNodeList,
container: OpaqueRoot,
parentComponent: ?React$Component<any, any>,
callback: ?Function,
): Lane {
const current = container.current;
const lane = requestUpdateLane(current);
if (enableSchedulingProfiler) {
markRenderScheduled(lane);
}
const context = getContextForSubtree(parentComponent);
if (container.context === null) {
container.context = context;
} else {
container.pendingContext = context;
}
const update = createUpdate(lane);
// Caution: React DevTools currently depends on this property
// being called "element".
update.payload = {element};

The argument of render() is stored in the update payload

const root = enqueueUpdate(current, update, lane);

Then the update is enqueued. We won't dive into how this is done

Just remember that the update is waiting to be processed.

if (root !== null) {
scheduleUpdateOnFiber(root, current, lane);
entangleTransitions(root, current, lane);
}
return lane;
}

3. Initial mount in Render phase

3.1 performConcurrentWorkOnRoot()

As mentioned in Overview of React internals, performConcurrentWorkOnRoot() is the entry point to start rendering for both initial mount and re-render.

One thing to keep in mind is that even it is named with concurrent, it internally still falls back to sync mode when necessary. Initial mount is one of the cases because the DefaultLane is blocking lane.

function performConcurrentWorkOnRoot(root, didTimeout) {
...
// Determine the next lanes to work on, using the fields stored
// on the root.
let lanes = getNextLanes(
root,
root === workInProgressRoot ? workInProgressRootRenderLanes : NoLanes,
);
...
// We disable time-slicing in some cases: if the work has been CPU-bound
// for too long ("expired" work, to prevent starvation), or we're in
// sync-updates-by-default mode.
// TODO: We only check `didTimeout` defensively, to account for a Scheduler
// bug we're still investigating. Once the bug in Scheduler is fixed,
// we can remove this, since we track expiration ourselves.
const shouldTimeSlice =
!includesBlockingLane(root, lanes) &&
!includesExpiredLane(root, lanes) &&
(disableSchedulerTimeoutInWorkLoop || !didTimeout);
let exitStatus = shouldTimeSlice
? renderRootConcurrent(root, lanes)
--------------------
: renderRootSync(root, lanes);
--------------
...
}
function performConcurrentWorkOnRoot(root, didTimeout) {
...
// Determine the next lanes to work on, using the fields stored
// on the root.
let lanes = getNextLanes(
root,
root === workInProgressRoot ? workInProgressRootRenderLanes : NoLanes,
);
...
// We disable time-slicing in some cases: if the work has been CPU-bound
// for too long ("expired" work, to prevent starvation), or we're in
// sync-updates-by-default mode.
// TODO: We only check `didTimeout` defensively, to account for a Scheduler
// bug we're still investigating. Once the bug in Scheduler is fixed,
// we can remove this, since we track expiration ourselves.
const shouldTimeSlice =
!includesBlockingLane(root, lanes) &&
!includesExpiredLane(root, lanes) &&
(disableSchedulerTimeoutInWorkLoop || !didTimeout);
let exitStatus = shouldTimeSlice
? renderRootConcurrent(root, lanes)
--------------------
: renderRootSync(root, lanes);
--------------
...
}
export function includesBlockingLane(root: FiberRoot, lanes: Lanes) {

Blocking means it is important, should not be interrupted.

const SyncDefaultLanes =
InputContinuousHydrationLane |
InputContinuousLane |
DefaultHydrationLane |
DefaultLane;
-----------

DefaultLane is blocking lane

return (lanes & SyncDefaultLanes) !== NoLanes;
}
export function includesBlockingLane(root: FiberRoot, lanes: Lanes) {

Blocking means it is important, should not be interrupted.

const SyncDefaultLanes =
InputContinuousHydrationLane |
InputContinuousLane |
DefaultHydrationLane |
DefaultLane;
-----------

DefaultLane is blocking lane

return (lanes & SyncDefaultLanes) !== NoLanes;
}

From above code we see that for initial mount, concurrent mode is actually not used. This makes sense, for initial mount, we should try to pain the UI as soon as possible, it doesn’t help to defer it.

3.2 renderRootSync()

renderRootSync() internally is just a while loop.

function renderRootSync(root: FiberRoot, lanes: Lanes) {
const prevExecutionContext = executionContext;
executionContext |= RenderContext;
const prevDispatcher = pushDispatcher();
// If the root or lanes have changed, throw out the existing stack
// and prepare a fresh one. Otherwise we'll continue where we left off.
if (workInProgressRoot !== root || workInProgressRootRenderLanes !== lanes) {
if (enableUpdaterTracking) {
if (isDevToolsPresent) {
const memoizedUpdaters = root.memoizedUpdaters;
if (memoizedUpdaters.size > 0) {
restorePendingUpdaters(root, workInProgressRootRenderLanes);
memoizedUpdaters.clear();
}
// At this point, move Fibers that scheduled the upcoming work from the Map to the Set.
// If we bailout on this work, we'll move them back (like above).
// It's important to move them now in case the work spawns more work at the same priority with different updaters.
// That way we can keep the current update and future updates separate.
movePendingFibersToMemoized(root, lanes);
}
}
workInProgressTransitions = getTransitionsForLanes(root, lanes);
prepareFreshStack(root, lanes);
-----------------
}
do {
try {
workLoopSync();
break;
} catch (thrownValue) {
handleError(root, thrownValue);
}
} while (true);
resetContextDependencies();
executionContext = prevExecutionContext;
popDispatcher(prevDispatcher);
// Set this to null to indicate there's no in-progress render.
workInProgressRoot = null;
workInProgressRootRenderLanes = NoLanes;
return workInProgressRootExitStatus;
}
// The work loop is an extremely hot path. Tell Closure not to inline it.
/** @noinline */
function workLoopSync() {
// Already timed out, so perform work without checking if we need to yield.
while (workInProgress !== null) {
---------------------------------

This while loop means if workInprogress exists,

it'll keep on performUnitOfWork() on it

performUnitOfWork(workInProgress);
-----------------

per its name, it works on one unit of Fiber Node

}
}
function renderRootSync(root: FiberRoot, lanes: Lanes) {
const prevExecutionContext = executionContext;
executionContext |= RenderContext;
const prevDispatcher = pushDispatcher();
// If the root or lanes have changed, throw out the existing stack
// and prepare a fresh one. Otherwise we'll continue where we left off.
if (workInProgressRoot !== root || workInProgressRootRenderLanes !== lanes) {
if (enableUpdaterTracking) {
if (isDevToolsPresent) {
const memoizedUpdaters = root.memoizedUpdaters;
if (memoizedUpdaters.size > 0) {
restorePendingUpdaters(root, workInProgressRootRenderLanes);
memoizedUpdaters.clear();
}
// At this point, move Fibers that scheduled the upcoming work from the Map to the Set.
// If we bailout on this work, we'll move them back (like above).
// It's important to move them now in case the work spawns more work at the same priority with different updaters.
// That way we can keep the current update and future updates separate.
movePendingFibersToMemoized(root, lanes);
}
}
workInProgressTransitions = getTransitionsForLanes(root, lanes);
prepareFreshStack(root, lanes);
-----------------
}
do {
try {
workLoopSync();
break;
} catch (thrownValue) {
handleError(root, thrownValue);
}
} while (true);
resetContextDependencies();
executionContext = prevExecutionContext;
popDispatcher(prevDispatcher);
// Set this to null to indicate there's no in-progress render.
workInProgressRoot = null;
workInProgressRootRenderLanes = NoLanes;
return workInProgressRootExitStatus;
}
// The work loop is an extremely hot path. Tell Closure not to inline it.
/** @noinline */
function workLoopSync() {
// Already timed out, so perform work without checking if we need to yield.
while (workInProgress !== null) {
---------------------------------

This while loop means if workInprogress exists,

it'll keep on performUnitOfWork() on it

performUnitOfWork(workInProgress);
-----------------

per its name, it works on one unit of Fiber Node

}
}

Here we need to explain what workInProgress means. In React code base prefixes of current and workInProgress are everywhere. Since React use Fiber Tree to represent current state internally, every time there is an update, React needs to construct a new tree and diff against the old. So current means the current version which is painted on the UI, workInProgress means the version that is the being built and will be used as next current.

3.3 performUnitOfWork()

This is where React works on single Fiber Node to see if there is anything to be done.

In order to understand this section more easily, I recommend you first check out my episode - How does React traverse Fiber tree internally.

function performUnitOfWork(unitOfWork: Fiber): void {
// The current, flushed, state of this fiber is the alternate. Ideally
// nothing should rely on this, but relying on it here means that we don't
// need an additional field on the work in progress.
const current = unitOfWork.alternate;
let next;
if (enableProfilerTimer && (unitOfWork.mode & ProfileMode) !== NoMode) {
startProfilerTimer(unitOfWork);
next = beginWork(current, unitOfWork, subtreeRenderLanes);
---------
stopProfilerTimerIfRunningAndRecordDelta(unitOfWork, true);
} else {
next = beginWork(current, unitOfWork, subtreeRenderLanes);
---------
}
resetCurrentDebugFiberInDEV();
unitOfWork.memoizedProps = unitOfWork.pendingProps;
if (next === null) {
// If this doesn't spawn new work, complete the current work.
completeUnitOfWork(unitOfWork);
} else {
workInProgress = next;

as mentioned, workLoopSync() is just a while loop

that keeps running completeUnitOfWork() on workInProgress

So assigning workInProgress here means setting next Fiber Node to work on

}
ReactCurrentOwner.current = null;
}
function performUnitOfWork(unitOfWork: Fiber): void {
// The current, flushed, state of this fiber is the alternate. Ideally
// nothing should rely on this, but relying on it here means that we don't
// need an additional field on the work in progress.
const current = unitOfWork.alternate;
let next;
if (enableProfilerTimer && (unitOfWork.mode & ProfileMode) !== NoMode) {
startProfilerTimer(unitOfWork);
next = beginWork(current, unitOfWork, subtreeRenderLanes);
---------
stopProfilerTimerIfRunningAndRecordDelta(unitOfWork, true);
} else {
next = beginWork(current, unitOfWork, subtreeRenderLanes);
---------
}
resetCurrentDebugFiberInDEV();
unitOfWork.memoizedProps = unitOfWork.pendingProps;
if (next === null) {
// If this doesn't spawn new work, complete the current work.
completeUnitOfWork(unitOfWork);
} else {
workInProgress = next;

as mentioned, workLoopSync() is just a while loop

that keeps running completeUnitOfWork() on workInProgress

So assigning workInProgress here means setting next Fiber Node to work on

}
ReactCurrentOwner.current = null;
}

beginWork() is where the actual rendering happens.

function beginWork(
current: Fiber | null,
workInProgress: Fiber,
renderLanes: Lanes,
): Fiber | null {
if (current !== null) {

If current is not null, meaning this is not initial mount

...
} else {
didReceiveUpdate = false

otherwise it is initial mount, so of course there is no update

...
}
switch (workInProgress.tag) {

It handles different types of elements differently

case IndeterminateComponent: {

IndeterminateComponent means the Class component or Function component

that hasn't been instantiated yet. Once it is rendered it is determined

with a right tag. We'll come back to this soon

return mountIndeterminateComponent(
current,
workInProgress,
workInProgress.type,
renderLanes,
);
}
case FunctionComponent: {

The custom function component we write

const Component = workInProgress.type;
const unresolvedProps = workInProgress.pendingProps;
const resolvedProps =
workInProgress.elementType === Component
? unresolvedProps
: resolveDefaultProps(Component, unresolvedProps);
return updateFunctionComponent(
current,
workInProgress,
Component,
resolvedProps,
renderLanes,
);
}
case HostRoot:

This is HostRoot under FiberRootNode

return updateHostRoot(current, workInProgress, renderLanes);
case HostComponent:

This is the intrinsic HTML tags, like p, div .etc

return updateHostComponent(current, workInProgress, renderLanes);
case HostText:

This HTML text node

return updateHostText(current, workInProgress);
case SuspenseComponent:
...

There are more types

}
}
function beginWork(
current: Fiber | null,
workInProgress: Fiber,
renderLanes: Lanes,
): Fiber | null {
if (current !== null) {

If current is not null, meaning this is not initial mount

...
} else {
didReceiveUpdate = false

otherwise it is initial mount, so of course there is no update

...
}
switch (workInProgress.tag) {

It handles different types of elements differently

case IndeterminateComponent: {

IndeterminateComponent means the Class component or Function component

that hasn't been instantiated yet. Once it is rendered it is determined

with a right tag. We'll come back to this soon

return mountIndeterminateComponent(
current,
workInProgress,
workInProgress.type,
renderLanes,
);
}
case FunctionComponent: {

The custom function component we write

const Component = workInProgress.type;
const unresolvedProps = workInProgress.pendingProps;
const resolvedProps =
workInProgress.elementType === Component
? unresolvedProps
: resolveDefaultProps(Component, unresolvedProps);
return updateFunctionComponent(
current,
workInProgress,
Component,
resolvedProps,
renderLanes,
);
}
case HostRoot:

This is HostRoot under FiberRootNode

return updateHostRoot(current, workInProgress, renderLanes);
case HostComponent:

This is the intrinsic HTML tags, like p, div .etc

return updateHostComponent(current, workInProgress, renderLanes);
case HostText:

This HTML text node

return updateHostText(current, workInProgress);
case SuspenseComponent:
...

There are more types

}
}

Now it’s time to go through the rendering steps.

3.4 prepareFreshStack()

In renderRootSync(), there is an important call of prepareFreshStack().

function prepareFreshStack(root: FiberRoot, lanes: Lanes): Fiber {
root.finishedWork = null;
root.finishedLanes = NoLanes;
...
workInProgressRoot = root;
const rootWorkInProgress = createWorkInProgress(root.current, null);

The current of root is the FiberNode of HostRoot

workInProgress = rootWorkInProgress;
workInProgressRootRenderLanes = subtreeRenderLanes = workInProgressRootIncludedLanes = lanes;
workInProgressRootExitStatus = RootInProgress;
workInProgressRootFatalError = null;
workInProgressRootSkippedLanes = NoLanes;
workInProgressRootInterleavedUpdatedLanes = NoLanes;
workInProgressRootRenderPhaseUpdatedLanes = NoLanes;
workInProgressRootPingedLanes = NoLanes;
workInProgressRootConcurrentErrors = null;
workInProgressRootRecoverableErrors = null;
finishQueueingConcurrentUpdates();
return rootWorkInProgress;
}
function prepareFreshStack(root: FiberRoot, lanes: Lanes): Fiber {
root.finishedWork = null;
root.finishedLanes = NoLanes;
...
workInProgressRoot = root;
const rootWorkInProgress = createWorkInProgress(root.current, null);

The current of root is the FiberNode of HostRoot

workInProgress = rootWorkInProgress;
workInProgressRootRenderLanes = subtreeRenderLanes = workInProgressRootIncludedLanes = lanes;
workInProgressRootExitStatus = RootInProgress;
workInProgressRootFatalError = null;
workInProgressRootSkippedLanes = NoLanes;
workInProgressRootInterleavedUpdatedLanes = NoLanes;
workInProgressRootRenderPhaseUpdatedLanes = NoLanes;
workInProgressRootPingedLanes = NoLanes;
workInProgressRootConcurrentErrors = null;
workInProgressRootRecoverableErrors = null;
finishQueueingConcurrentUpdates();
return rootWorkInProgress;
}

So every time a fresh rendering starts, a new workInProgress is created from the current HostRoot. It works as the root of new Fiber Tree.

Thus for the branches inside beginWork(), we’ll first go to HostRoot and updateHostRoot() is next step.

3.5 updateHostRoot()

function updateHostRoot(
current: null | Fiber,
workInProgress: Fiber,
renderLanes: Lanes,
) {
pushHostRootContext(workInProgress);
const nextProps = workInProgress.pendingProps;
const prevState = workInProgress.memoizedState;
const prevChildren = prevState.element;
cloneUpdateQueue(current, workInProgress);
processUpdateQueue(workInProgress, nextProps, null, renderLanes);
------------------

This call processes the update mentioned at the beginning of this post

Just keep in mind that the update scheduled is processed

and the payload is extracted, the element is assigned as memoizedState

const nextState: RootState = workInProgress.memoizedState;
const root: FiberRoot = workInProgress.stateNode;
pushRootTransition(workInProgress, root, renderLanes);
if (enableTransitionTracing) {
pushRootMarkerInstance(workInProgress);
}
// Caution: React DevTools currently depends on this property
// being called "element".
const nextChildren = nextState.element;
------------

We're able to get the argument of ReactDOMRoot.render()!

if (supportsHydration && prevState.isDehydrated) {
...
} else {
// Root is not dehydrated. Either this is a client-only root, or it
// already hydrated.
resetHydrationState();
if (nextChildren === prevChildren) {
return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
}
reconcileChildren(current, workInProgress, nextChildren, renderLanes);

Here current and workInprogress both don't have child

And nextChildren is <App/>

}
return workInProgress.child;

After reconciling, new child is created for workInProgress

Returning here means that workLoopSync() will handle it next

}
function updateHostRoot(
current: null | Fiber,
workInProgress: Fiber,
renderLanes: Lanes,
) {
pushHostRootContext(workInProgress);
const nextProps = workInProgress.pendingProps;
const prevState = workInProgress.memoizedState;
const prevChildren = prevState.element;
cloneUpdateQueue(current, workInProgress);
processUpdateQueue(workInProgress, nextProps, null, renderLanes);
------------------

This call processes the update mentioned at the beginning of this post

Just keep in mind that the update scheduled is processed

and the payload is extracted, the element is assigned as memoizedState

const nextState: RootState = workInProgress.memoizedState;
const root: FiberRoot = workInProgress.stateNode;
pushRootTransition(workInProgress, root, renderLanes);
if (enableTransitionTracing) {
pushRootMarkerInstance(workInProgress);
}
// Caution: React DevTools currently depends on this property
// being called "element".
const nextChildren = nextState.element;
------------

We're able to get the argument of ReactDOMRoot.render()!

if (supportsHydration && prevState.isDehydrated) {
...
} else {
// Root is not dehydrated. Either this is a client-only root, or it
// already hydrated.
resetHydrationState();
if (nextChildren === prevChildren) {
return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
}
reconcileChildren(current, workInProgress, nextChildren, renderLanes);

Here current and workInprogress both don't have child

And nextChildren is <App/>

}
return workInProgress.child;

After reconciling, new child is created for workInProgress

Returning here means that workLoopSync() will handle it next

}

3.6 reconcileChildren()

This is a very import function in React internals. Per its name, roughly we can think of reconcile as diff. It compares new children against old children, and sets the right child on workInProgress.

export function reconcileChildren(
current: Fiber | null,
workInProgress: Fiber,
nextChildren: any,
renderLanes: Lanes,
) {
if (current === null) {

if there is no current, meaning this is initial mount

// If this is a fresh new component that hasn't been rendered yet, we
// won't update its child set by applying minimal side-effects. Instead,
// we will add them all to the child before it gets rendered. That means
// we can optimize this reconciliation pass by not tracking side-effects.
workInProgress.child = mountChildFibers(
----------------
workInProgress,
null,
nextChildren,
renderLanes,
);
} else {

if there is current, meaning this is re-render, so reconcile

// If the current child is the same as the work in progress, it means that
// we haven't yet started any work on these children. Therefore, we use
// the clone algorithm to create a copy of all the current children.
// If we had any progressed work already, that is invalid at this point so
// let's throw it out.
workInProgress.child = reconcileChildFibers(
--------------------
workInProgress,
current.child,
nextChildren,
renderLanes,
);
}
}
export function reconcileChildren(
current: Fiber | null,
workInProgress: Fiber,
nextChildren: any,
renderLanes: Lanes,
) {
if (current === null) {

if there is no current, meaning this is initial mount

// If this is a fresh new component that hasn't been rendered yet, we
// won't update its child set by applying minimal side-effects. Instead,
// we will add them all to the child before it gets rendered. That means
// we can optimize this reconciliation pass by not tracking side-effects.
workInProgress.child = mountChildFibers(
----------------
workInProgress,
null,
nextChildren,
renderLanes,
);
} else {

if there is current, meaning this is re-render, so reconcile

// If the current child is the same as the work in progress, it means that
// we haven't yet started any work on these children. Therefore, we use
// the clone algorithm to create a copy of all the current children.
// If we had any progressed work already, that is invalid at this point so
// let's throw it out.
workInProgress.child = reconcileChildFibers(
--------------------
workInProgress,
current.child,
nextChildren,
renderLanes,
);
}
}

As mentioned above, FiberRootNode always has current, so we go to the 2nd branch - reconcileChildFibers. But since this is the initial mount, its child current.child is null.

Also notice that we are setting child on workInProgress, since workInProgress is being constructed and it doesn’t have child yet.

3.7 reconcileChildFibers() vs mountChildFibers().

The goal of reconcile is to try to reuse stuff we already have, we can treat mount as a special primitive version of reconcile, in which we always refresh everything.

In fact in the code these two are not that different, they are same closures but with slight different flag shouldTrackSideEffects.

export const reconcileChildFibers: ChildReconciler =
createChildReconciler(true);
---------------------------
export const mountChildFibers: ChildReconciler = createChildReconciler(false);
---------------------------
function createChildReconciler(
shouldTrackSideEffects: boolean,

This flag controls if we need to track insertions .etc

): ChildReconciler {
...
function reconcileChildFibers(
returnFiber: Fiber,
currentFirstChild: Fiber | null,
newChild: any,
lanes: Lanes,
): Fiber | null {
// This indirection only exists so we can reset `thenableState` at the end.
// It should get inlined by Closure.
thenableIndexCounter = 0;
const firstChildFiber = reconcileChildFibersImpl(
returnFiber,
currentFirstChild,
newChild,
lanes,
);
thenableState = null;
// Don't bother to reset `thenableIndexCounter` to 0 because it always gets
// set at the beginning.
return firstChildFiber;

The first child fiber after reconciling children will be returned

and set as child of workInprogress

}
return reconcileChildFibers;
}
export const reconcileChildFibers: ChildReconciler =
createChildReconciler(true);
---------------------------
export const mountChildFibers: ChildReconciler = createChildReconciler(false);
---------------------------
function createChildReconciler(
shouldTrackSideEffects: boolean,

This flag controls if we need to track insertions .etc

): ChildReconciler {
...
function reconcileChildFibers(
returnFiber: Fiber,
currentFirstChild: Fiber | null,
newChild: any,
lanes: Lanes,
): Fiber | null {
// This indirection only exists so we can reset `thenableState` at the end.
// It should get inlined by Closure.
thenableIndexCounter = 0;
const firstChildFiber = reconcileChildFibersImpl(
returnFiber,
currentFirstChild,
newChild,
lanes,
);
thenableState = null;
// Don't bother to reset `thenableIndexCounter` to 0 because it always gets
// set at the beginning.
return firstChildFiber;

The first child fiber after reconciling children will be returned

and set as child of workInprogress

}
return reconcileChildFibers;
}

Imagine if we have a full fiber tree to be constructed, all nodes should be marked as “needed to insert” after reconciling, right? But it is obviously not necessary, we only need to insert the root and that’s it! So this mountChildFibers is actually a internal improvement to make things more explicit.

function reconcileChildFibersImpl(
returnFiber: Fiber,
currentFirstChild: Fiber | null,
newChild: any,
lanes: Lanes,
): Fiber | null {
// This function is not recursive.
// If the top level item is an array, we treat it as a set of children,
// not as a fragment. Nested arrays on the other hand will be treated as
// fragment nodes. Recursion happens at the normal flow.
// Handle top level unkeyed fragments as if they were arrays.
// This leads to an ambiguity between <>{[...]}</> and <>...</>.
// We treat the ambiguous cases above the same.
// TODO: Let's use recursion like we do for Usable nodes?
const isUnkeyedTopLevelFragment =
typeof newChild === 'object' &&
newChild !== null &&
newChild.type === REACT_FRAGMENT_TYPE &&
newChild.key === null;
if (isUnkeyedTopLevelFragment) {
newChild = newChild.props.children;
}
// Handle object types
if (typeof newChild === 'object' && newChild !== null) {
switch (newChild.$$typeof) {

This $$typeof is the typeof React Element

case REACT_ELEMENT_TYPE:
return placeSingleChild(
----------------
reconcileSingleElement(
----------------------

We've dive into these 2 functions next

returnFiber,
currentFirstChild,
newChild,
lanes,
),
);

if children is React Element, like <App/>

case REACT_PORTAL_TYPE:
return placeSingleChild(
reconcileSinglePortal(
returnFiber,
currentFirstChild,
newChild,
lanes,
),
);
case REACT_LAZY_TYPE:
const payload = newChild._payload;
const init = newChild._init;
// TODO: This function is supposed to be non-recursive.
return reconcileChildFibers(
returnFiber,
currentFirstChild,
init(payload),
lanes,
);
}
if (isArray(newChild)) {
-----------------

if children is array

return reconcileChildrenArray(
returnFiber,
currentFirstChild,
newChild,
lanes,
);
}
if (getIteratorFn(newChild)) {
return reconcileChildrenIterator(
returnFiber,
currentFirstChild,
newChild,
lanes,
);
}
if (typeof newChild.then === 'function') {
const thenable: Thenable<any> = (newChild: any);
return reconcileChildFibersImpl(
returnFiber,
currentFirstChild,
unwrapThenable(thenable),
lanes,
);
}
if (
newChild.$$typeof === REACT_CONTEXT_TYPE ||
newChild.$$typeof === REACT_SERVER_CONTEXT_TYPE
) {
...
}
throwOnInvalidObjectType(returnFiber, newChild);
}
if (
(typeof newChild === 'string' && newChild !== '') ||
typeof newChild === 'number'
) {
return placeSingleChild(
reconcileSingleTextNode(
returnFiber,
currentFirstChild,
'' + newChild,
lanes,
),
);
}

This handles the most primitive case - updating the Text Node

// Remaining cases are all treated as empty.
return deleteRemainingChildren(returnFiber, currentFirstChild);
}
function reconcileChildFibersImpl(
returnFiber: Fiber,
currentFirstChild: Fiber | null,
newChild: any,
lanes: Lanes,
): Fiber | null {
// This function is not recursive.
// If the top level item is an array, we treat it as a set of children,
// not as a fragment. Nested arrays on the other hand will be treated as
// fragment nodes. Recursion happens at the normal flow.
// Handle top level unkeyed fragments as if they were arrays.
// This leads to an ambiguity between <>{[...]}</> and <>...</>.
// We treat the ambiguous cases above the same.
// TODO: Let's use recursion like we do for Usable nodes?
const isUnkeyedTopLevelFragment =
typeof newChild === 'object' &&
newChild !== null &&
newChild.type === REACT_FRAGMENT_TYPE &&
newChild.key === null;
if (isUnkeyedTopLevelFragment) {
newChild = newChild.props.children;
}
// Handle object types
if (typeof newChild === 'object' && newChild !== null) {
switch (newChild.$$typeof) {

This $$typeof is the typeof React Element

case REACT_ELEMENT_TYPE:
return placeSingleChild(
----------------
reconcileSingleElement(
----------------------

We've dive into these 2 functions next

returnFiber,
currentFirstChild,
newChild,
lanes,
),
);

if children is React Element, like <App/>

case REACT_PORTAL_TYPE:
return placeSingleChild(
reconcileSinglePortal(
returnFiber,
currentFirstChild,
newChild,
lanes,
),
);
case REACT_LAZY_TYPE:
const payload = newChild._payload;
const init = newChild._init;
// TODO: This function is supposed to be non-recursive.
return reconcileChildFibers(
returnFiber,
currentFirstChild,
init(payload),
lanes,
);
}
if (isArray(newChild)) {
-----------------

if children is array

return reconcileChildrenArray(
returnFiber,
currentFirstChild,
newChild,
lanes,
);
}
if (getIteratorFn(newChild)) {
return reconcileChildrenIterator(
returnFiber,
currentFirstChild,
newChild,
lanes,
);
}
if (typeof newChild.then === 'function') {
const thenable: Thenable<any> = (newChild: any);
return reconcileChildFibersImpl(
returnFiber,
currentFirstChild,
unwrapThenable(thenable),
lanes,
);
}
if (
newChild.$$typeof === REACT_CONTEXT_TYPE ||
newChild.$$typeof === REACT_SERVER_CONTEXT_TYPE
) {
...
}
throwOnInvalidObjectType(returnFiber, newChild);
}
if (
(typeof newChild === 'string' && newChild !== '') ||
typeof newChild === 'number'
) {
return placeSingleChild(
reconcileSingleTextNode(
returnFiber,
currentFirstChild,
'' + newChild,
lanes,
),
);
}

This handles the most primitive case - updating the Text Node

// Remaining cases are all treated as empty.
return deleteRemainingChildren(returnFiber, currentFirstChild);
}

We see that there are two steps. reconcileXXX() to do the diffing, and placeSingleChild() to mark that the fiber needs insertion in DOM.

3.8 reconcileSingleElement()

function reconcileSingleElement(
returnFiber: Fiber,
currentFirstChild: Fiber | null,
element: ReactElement,
lanes: Lanes,
): Fiber {
const key = element.key;
let child = currentFirstChild;
while (child !== null) {
----------------

This handles update if there is already child

But under initial mount, there isn't, so we'll ignore it for now

// TODO: If key === null and child.key === null, then this only applies to
// the first item in the list.
if (child.key === key) {
const elementType = element.type;
if (elementType === REACT_FRAGMENT_TYPE) {
if (child.tag === Fragment) {
deleteRemainingChildren(returnFiber, child.sibling);
const existing = useFiber(child, element.props.children);
existing.return = returnFiber;
return existing;
}
} else {
if (
child.elementType === elementType ||
// Keep this check inline so it only runs on the false path:
(__DEV__
? isCompatibleFamilyForHotReloading(child, element)
: false) ||
// Lazy types should reconcile their resolved type.
// We need to do this after the Hot Reloading check above,
// because hot reloading has different semantics than prod because
// it doesn't resuspend. So we can't let the call below suspend.
(typeof elementType === 'object' &&
elementType !== null &&
elementType.$$typeof === REACT_LAZY_TYPE &&
resolveLazy(elementType) === child.type)
) {
deleteRemainingChildren(returnFiber, child.sibling);
const existing = useFiber(child, element.props);
existing.ref = coerceRef(returnFiber, child, element);
existing.return = returnFiber;
if (__DEV__) {
existing._debugSource = element._source;
existing._debugOwner = element._owner;
}
return existing;
}
}
// Didn't match.
deleteRemainingChildren(returnFiber, child);
break;
} else {
deleteChild(returnFiber, child);
}
child = child.sibling;
}
if (element.type === REACT_FRAGMENT_TYPE) {
const created = createFiberFromFragment(
element.props.children,
returnFiber.mode,
lanes,
element.key,
);
created.return = returnFiber;
return created;
} else {
const created = createFiberFromElement(element, returnFiber.mode, lanes);

Since there is no previous version, we just create a new fiber from the element

created.ref = coerceRef(returnFiber, currentFirstChild, element);
created.return = returnFiber;
return created;
}
}
function reconcileSingleElement(
returnFiber: Fiber,
currentFirstChild: Fiber | null,
element: ReactElement,
lanes: Lanes,
): Fiber {
const key = element.key;
let child = currentFirstChild;
while (child !== null) {
----------------

This handles update if there is already child

But under initial mount, there isn't, so we'll ignore it for now

// TODO: If key === null and child.key === null, then this only applies to
// the first item in the list.
if (child.key === key) {
const elementType = element.type;
if (elementType === REACT_FRAGMENT_TYPE) {
if (child.tag === Fragment) {
deleteRemainingChildren(returnFiber, child.sibling);
const existing = useFiber(child, element.props.children);
existing.return = returnFiber;
return existing;
}
} else {
if (
child.elementType === elementType ||
// Keep this check inline so it only runs on the false path:
(__DEV__
? isCompatibleFamilyForHotReloading(child, element)
: false) ||
// Lazy types should reconcile their resolved type.
// We need to do this after the Hot Reloading check above,
// because hot reloading has different semantics than prod because
// it doesn't resuspend. So we can't let the call below suspend.
(typeof elementType === 'object' &&
elementType !== null &&
elementType.$$typeof === REACT_LAZY_TYPE &&
resolveLazy(elementType) === child.type)
) {
deleteRemainingChildren(returnFiber, child.sibling);
const existing = useFiber(child, element.props);
existing.ref = coerceRef(returnFiber, child, element);
existing.return = returnFiber;
if (__DEV__) {
existing._debugSource = element._source;
existing._debugOwner = element._owner;
}
return existing;
}
}
// Didn't match.
deleteRemainingChildren(returnFiber, child);
break;
} else {
deleteChild(returnFiber, child);
}
child = child.sibling;
}
if (element.type === REACT_FRAGMENT_TYPE) {
const created = createFiberFromFragment(
element.props.children,
returnFiber.mode,
lanes,
element.key,
);
created.return = returnFiber;
return created;
} else {
const created = createFiberFromElement(element, returnFiber.mode, lanes);

Since there is no previous version, we just create a new fiber from the element

created.ref = coerceRef(returnFiber, currentFirstChild, element);
created.return = returnFiber;
return created;
}
}

reconcileSingleElement() for initial mount is quite simple as we see. Notice that the newly created Fiber Node will be child on workInProgress.

One thing to notice is that when Fiber Node is created from custom component, its tag is IndeterminateComponent, not FunctionComponent yet.

export function createFiberFromElement(
element: ReactElement,
mode: TypeOfMode,
lanes: Lanes,
): Fiber {
let owner = null;
const type = element.type;
const key = element.key;
const pendingProps = element.props;
const fiber = createFiberFromTypeAndProps(
type,
key,
pendingProps,
owner,
mode,
lanes,
);
return fiber;
}
export function createFiberFromTypeAndProps(
type: any, // React$ElementType
key: null | string,
pendingProps: any,
owner: null | Fiber,
mode: TypeOfMode,
lanes: Lanes,
): Fiber {
let fiberTag = IndeterminateComponent;
// The resolved type is set if we know what the final type will be. I.e. it's not lazy.
let resolvedType = type;
...
const fiber = createFiber(fiberTag, pendingProps, key, mode);
fiber.elementType = type;
fiber.type = resolvedType;
fiber.lanes = lanes;
return fiber;
}
export function createFiberFromElement(
element: ReactElement,
mode: TypeOfMode,
lanes: Lanes,
): Fiber {
let owner = null;
const type = element.type;
const key = element.key;
const pendingProps = element.props;
const fiber = createFiberFromTypeAndProps(
type,
key,
pendingProps,
owner,
mode,
lanes,
);
return fiber;
}
export function createFiberFromTypeAndProps(
type: any, // React$ElementType
key: null | string,
pendingProps: any,
owner: null | Fiber,
mode: TypeOfMode,
lanes: Lanes,
): Fiber {
let fiberTag = IndeterminateComponent;
// The resolved type is set if we know what the final type will be. I.e. it's not lazy.
let resolvedType = type;
...
const fiber = createFiber(fiberTag, pendingProps, key, mode);
fiber.elementType = type;
fiber.type = resolvedType;
fiber.lanes = lanes;
return fiber;
}

3.9 placeSingleChild()

reconcileSingleElement() only does Fiber Node reconciliation, and placeSingleChild() is where child Fiber Node is marked to do insertion on DOM.

function placeSingleChild(newFiber: Fiber): Fiber {
// This is simpler for the single child case. We only need to do a
// placement for inserting new children.
if (shouldTrackSideEffects && newFiber.alternate === null) {

Yes, the flag is used here(well in some other places as well)

newFiber.flags |= Placement | PlacementDEV;
------------------

Placement means the DOM sub-tree needs to be inserted

}
return newFiber;
}
function placeSingleChild(newFiber: Fiber): Fiber {
// This is simpler for the single child case. We only need to do a
// placement for inserting new children.
if (shouldTrackSideEffects && newFiber.alternate === null) {

Yes, the flag is used here(well in some other places as well)

newFiber.flags |= Placement | PlacementDEV;
------------------

Placement means the DOM sub-tree needs to be inserted

}
return newFiber;
}

Notice that this is done on child, meaning in initial mount, the child of HostRoot is marked with Placement. In our demo code, it is <App/>.

3.10 mountIndeterminateComponent()

Next branch in beginWork() we’ll look at is IndeterminateComponent. Since <App/> is under HostRoot, and as mentioned already that Custom components are initially marked as IndeterminateComponent, we’ll come here when when first time <App/> is reconciled.

function mountIndeterminateComponent(
_current: null | Fiber,
workInProgress: Fiber,
Component: $FlowFixMe,
renderLanes: Lanes,
) {
resetSuspendedCurrentOnMountInLegacyMode(_current, workInProgress);
const props = workInProgress.pendingProps;
let context;
if (!disableLegacyContext) {
const unmaskedContext = getUnmaskedContext(
workInProgress,
Component,
false,
);
context = getMaskedContext(workInProgress, unmaskedContext);
}
prepareToReadContext(workInProgress, renderLanes);
let value;
let hasId;
value = renderWithHooks(

This runs function components and return the children elements

null,
workInProgress,
Component,
props,
context,
renderLanes,
);
hasId = checkDidRenderIdHook();
// React DevTools reads this flag.
workInProgress.flags |= PerformedWork;
if (
// Run these checks in production only if the flag is off.
// Eventually we'll delete this branch altogether.
!disableModulePatternComponents &&
typeof value === 'object' &&
value !== null &&
typeof value.render === 'function' &&
value.$$typeof === undefined
) {
// Proceed under the assumption that this is a class instance
workInProgress.tag = ClassComponent;

Once rendered, it is no long IndeterminateComponent

...
} else {
// Proceed under the assumption that this is a function component
workInProgress.tag = FunctionComponent;

Once rendered, it is no long IndeterminateComponent

if (getIsHydrating() && hasId) {
pushMaterializedTreeId(workInProgress);
}
reconcileChildren(null, workInProgress, value, renderLanes);
-----------------------------------------------------------

since here current is null, so mountChildFibers() will be used

return workInProgress.child;
}
}
function mountIndeterminateComponent(
_current: null | Fiber,
workInProgress: Fiber,
Component: $FlowFixMe,
renderLanes: Lanes,
) {
resetSuspendedCurrentOnMountInLegacyMode(_current, workInProgress);
const props = workInProgress.pendingProps;
let context;
if (!disableLegacyContext) {
const unmaskedContext = getUnmaskedContext(
workInProgress,
Component,
false,
);
context = getMaskedContext(workInProgress, unmaskedContext);
}
prepareToReadContext(workInProgress, renderLanes);
let value;
let hasId;
value = renderWithHooks(

This runs function components and return the children elements

null,
workInProgress,
Component,
props,
context,
renderLanes,
);
hasId = checkDidRenderIdHook();
// React DevTools reads this flag.
workInProgress.flags |= PerformedWork;
if (
// Run these checks in production only if the flag is off.
// Eventually we'll delete this branch altogether.
!disableModulePatternComponents &&
typeof value === 'object' &&
value !== null &&
typeof value.render === 'function' &&
value.$$typeof === undefined
) {
// Proceed under the assumption that this is a class instance
workInProgress.tag = ClassComponent;

Once rendered, it is no long IndeterminateComponent

...
} else {
// Proceed under the assumption that this is a function component
workInProgress.tag = FunctionComponent;

Once rendered, it is no long IndeterminateComponent

if (getIsHydrating() && hasId) {
pushMaterializedTreeId(workInProgress);
}
reconcileChildren(null, workInProgress, value, renderLanes);
-----------------------------------------------------------

since here current is null, so mountChildFibers() will be used

return workInProgress.child;
}
}

As mentioned, when rendering <App/>, mountChildFibers() is used because different from HostRoot which always has current, there is no previous version for <App/> and placeSingleChild() ignores the insertion flags.

App() returns <div/>, which later is handled by HostComponent branch in beginWork().

3.11 updateHostComponent()

function updateHostComponent(
current: Fiber | null,
workInProgress: Fiber,
renderLanes: Lanes,
) {
pushHostContext(workInProgress);
if (current === null) {
tryToClaimNextHydratableInstance(workInProgress);
}
const type = workInProgress.type;
const nextProps = workInProgress.pendingProps;

pendingProps holds the children of <div/>, which is the <p/>

const prevProps = current !== null ? current.memoizedProps : null;
let nextChildren = nextProps.children;
const isDirectTextChild = shouldSetTextContent(type, nextProps);

This is an improvement if its children are static text, as in <a/>

if (isDirectTextChild) {
// We special case a direct text child of a host node. This is a common
// case. We won't handle it as a reified child. We will instead handle
// this in the host environment that also has access to this prop. That
// avoids allocating another HostText fiber and traversing it.
nextChildren = null;
} else if (prevProps !== null && shouldSetTextContent(type, prevProps)) {
// If we're switching from a direct text child to a normal child, or to
// empty, we need to schedule the text content to be reset.
workInProgress.flags |= ContentReset;
}
...
markRef(current, workInProgress);
reconcileChildren(current, workInProgress, nextChildren, renderLanes);
return workInProgress.child;
}
function updateHostComponent(
current: Fiber | null,
workInProgress: Fiber,
renderLanes: Lanes,
) {
pushHostContext(workInProgress);
if (current === null) {
tryToClaimNextHydratableInstance(workInProgress);
}
const type = workInProgress.type;
const nextProps = workInProgress.pendingProps;

pendingProps holds the children of <div/>, which is the <p/>

const prevProps = current !== null ? current.memoizedProps : null;
let nextChildren = nextProps.children;
const isDirectTextChild = shouldSetTextContent(type, nextProps);

This is an improvement if its children are static text, as in <a/>

if (isDirectTextChild) {
// We special case a direct text child of a host node. This is a common
// case. We won't handle it as a reified child. We will instead handle
// this in the host environment that also has access to this prop. That
// avoids allocating another HostText fiber and traversing it.
nextChildren = null;
} else if (prevProps !== null && shouldSetTextContent(type, prevProps)) {
// If we're switching from a direct text child to a normal child, or to
// empty, we need to schedule the text content to be reset.
workInProgress.flags |= ContentReset;
}
...
markRef(current, workInProgress);
reconcileChildren(current, workInProgress, nextChildren, renderLanes);
return workInProgress.child;
}

Above process repeats for <p/>, except that nextChildren now is an array, so reconcileChildrenArray() kicks in inside reconcileChildFibers().

reconcileChildrenArray() is a bit more complex because of the existence of key, for more details go to my another blog post - How does ‘key’ work internally? List diffing in React.

Other than the key handling, it basically return the first child fiber and continues, the siblings will be processed later since React flattens the tree structure into linked list. For more, please refer to How does React traverse Fiber tree internally.

For <Link/>, we are repeating the process as <App/>.

For <a/> and <button/> we’ll go to deeper to its text.

But they are a bit different, <a/> has a static text as children, while <button/> has some JSX expression {count}. That’s why in above code, <a/> has nextChildren as null, but <button/> continues to children.

3.12 updateHostText()

For <button/> its children is an array - ["click me - ", "0"], updateHostText() is the branch for both of them in beginWork().

function updateHostText(current, workInProgress) {
if (current === null) {
tryToClaimNextHydratableInstance(workInProgress);
} // Nothing to do here. This is terminal. We'll do the completion step
// immediately after.
return null;
}
function updateHostText(current, workInProgress) {
if (current === null) {
tryToClaimNextHydratableInstance(workInProgress);
} // Nothing to do here. This is terminal. We'll do the completion step
// immediately after.
return null;
}

But it does nothing except handling hydration. How the text in <a/> and <button/> are handled is in the Commit phase.

3.13 DOM nodes are created offscreen in completeWork().

As mentioned in How does React traverse Fiber tree internally, completeWork() is called on a fiber before its sibling is tried with beginWork().

Fiber Node has one important property - stateNode, which for intrinsic HTML tags, it refers to the actual DOM node. And the actual creation of DOM node is done in completeWork().

function completeWork(
current: Fiber | null,
workInProgress: Fiber,
renderLanes: Lanes,
): Fiber | null {
const newProps = workInProgress.pendingProps;
// Note: This intentionally doesn't check if we're hydrating because comparing
// to the current tree provider fiber is just as fast and less error-prone.
// Ideally we would have a special version of the work loop only
// for hydration.
popTreeContext(workInProgress);
switch (workInProgress.tag) {
case IndeterminateComponent:
case LazyComponent:
case SimpleMemoComponent:
case FunctionComponent:
case ForwardRef:
case Fragment:
case Mode:
case Profiler:
case ContextConsumer:
case MemoComponent:
bubbleProperties(workInProgress);
return null;
case ClassComponent: {
const Component = workInProgress.type;
if (isLegacyContextProvider(Component)) {
popLegacyContext(workInProgress);
}
bubbleProperties(workInProgress);
return null;
}
case HostRoot: {
...
return null;
}
...
case HostComponent: {

For HTMl tags

popHostContext(workInProgress);
const type = workInProgress.type;
if (current !== null && workInProgress.stateNode != null) {
----------------

If there is current version, then we go to update branch

updateHostComponent(
current,
workInProgress,
type,
newProps,
renderLanes,
);
if (current.ref !== workInProgress.ref) {
markRef(workInProgress);
}
} else {
----

But we don't have current version yet, so we go to this mount branch

...
if (wasHydrated) {
...
} else {
const rootContainerInstance = getRootHostContainer();
const instance = createInstance(

The actual DOM node

type,
newProps,
rootContainerInstance,
currentHostContext,
workInProgress,
);
appendAllChildren(instance, workInProgress, false, false);
-----------------

This is important, when an DOM node is created

It needs to be the parent of all direct connected DOM nodes of subtree

workInProgress.stateNode = instance;
---------
if (
finalizeInitialChildren(
-----------------------

This will be covered soon!

instance,
type,
newProps,
rootContainerInstance,
currentHostContext,
)
) {
markUpdate(workInProgress);
}
}
if (workInProgress.ref !== null) {
// If there is a ref on a host node we need to schedule a callback
markRef(workInProgress);
}
}
bubbleProperties(workInProgress);
...
return null;
}
case HostText: {
const newText = newProps;
if (current && workInProgress.stateNode != null) {
const oldText = current.memoizedProps;
// If we have an alternate, that means this is an update and we need
// to schedule a side-effect to do the updates.
updateHostText(current, workInProgress, oldText, newText);
} else {
...
const rootContainerInstance = getRootHostContainer();
const currentHostContext = getHostContext();
const wasHydrated = popHydrationState(workInProgress);
if (wasHydrated) {
if (prepareToHydrateHostTextInstance(workInProgress)) {
markUpdate(workInProgress);
}
} else {
workInProgress.stateNode = createTextInstance(
newText,
rootContainerInstance,
currentHostContext,
workInProgress,
);
}
}
bubbleProperties(workInProgress);
return null;
}
...
}
}
function completeWork(
current: Fiber | null,
workInProgress: Fiber,
renderLanes: Lanes,
): Fiber | null {
const newProps = workInProgress.pendingProps;
// Note: This intentionally doesn't check if we're hydrating because comparing
// to the current tree provider fiber is just as fast and less error-prone.
// Ideally we would have a special version of the work loop only
// for hydration.
popTreeContext(workInProgress);
switch (workInProgress.tag) {
case IndeterminateComponent:
case LazyComponent:
case SimpleMemoComponent:
case FunctionComponent:
case ForwardRef:
case Fragment:
case Mode:
case Profiler:
case ContextConsumer:
case MemoComponent:
bubbleProperties(workInProgress);
return null;
case ClassComponent: {
const Component = workInProgress.type;
if (isLegacyContextProvider(Component)) {
popLegacyContext(workInProgress);
}
bubbleProperties(workInProgress);
return null;
}
case HostRoot: {
...
return null;
}
...
case HostComponent: {

For HTMl tags

popHostContext(workInProgress);
const type = workInProgress.type;
if (current !== null && workInProgress.stateNode != null) {
----------------

If there is current version, then we go to update branch

updateHostComponent(
current,
workInProgress,
type,
newProps,
renderLanes,
);
if (current.ref !== workInProgress.ref) {
markRef(workInProgress);
}
} else {
----

But we don't have current version yet, so we go to this mount branch

...
if (wasHydrated) {
...
} else {
const rootContainerInstance = getRootHostContainer();
const instance = createInstance(

The actual DOM node

type,
newProps,
rootContainerInstance,
currentHostContext,
workInProgress,
);
appendAllChildren(instance, workInProgress, false, false);
-----------------

This is important, when an DOM node is created

It needs to be the parent of all direct connected DOM nodes of subtree

workInProgress.stateNode = instance;
---------
if (
finalizeInitialChildren(
-----------------------

This will be covered soon!

instance,
type,
newProps,
rootContainerInstance,
currentHostContext,
)
) {
markUpdate(workInProgress);
}
}
if (workInProgress.ref !== null) {
// If there is a ref on a host node we need to schedule a callback
markRef(workInProgress);
}
}
bubbleProperties(workInProgress);
...
return null;
}
case HostText: {
const newText = newProps;
if (current && workInProgress.stateNode != null) {
const oldText = current.memoizedProps;
// If we have an alternate, that means this is an update and we need
// to schedule a side-effect to do the updates.
updateHostText(current, workInProgress, oldText, newText);
} else {
...
const rootContainerInstance = getRootHostContainer();
const currentHostContext = getHostContext();
const wasHydrated = popHydrationState(workInProgress);
if (wasHydrated) {
if (prepareToHydrateHostTextInstance(workInProgress)) {
markUpdate(workInProgress);
}
} else {
workInProgress.stateNode = createTextInstance(
newText,
rootContainerInstance,
currentHostContext,
workInProgress,
);
}
}
bubbleProperties(workInProgress);
return null;
}
...
}
}

One question we left out was the different between <a/> and <button/>, they handled text node differently.

For <button/> we see that it goes to above HostText branch and createTextInstance() creates new text node. But for <a/> on the other hand it is a bit different. Let’s go deeper.

Notice in above code, there is finalizeInitialChildren() on HostComponent.

export function finalizeInitialChildren(
domElement: Instance,
type: string,
props: Props,
hostContext: HostContext,
): boolean {
setInitialProperties(domElement, type, props);
--------------------
...
}
export function setInitialProperties(
domElement: Element,
tag: string,
rawProps: Object,
rootContainerElement: Element | Document | DocumentFragment,
): void {
...
setInitialDOMProperties(
-----------------------
tag,
domElement,
rootContainerElement,
props,
isCustomComponentTag,
);
...
}
function setInitialDOMProperties(
tag: string,
domElement: Element,
rootContainerElement: Element | Document | DocumentFragment,
nextProps: Object,
isCustomComponentTag: boolean,
): void {
for (const propKey in nextProps) {
if (!nextProps.hasOwnProperty(propKey)) {
continue;
}
const nextProp = nextProps[propKey];
if (propKey === STYLE) {
// Relies on `updateStylesByID` not mutating `styleUpdates`.
setValueForStyles(domElement, nextProp);
} else if (propKey === DANGEROUSLY_SET_INNER_HTML) {
const nextHtml = nextProp ? nextProp[HTML] : undefined;
if (nextHtml != null) {
setInnerHTML(domElement, nextHtml);
}
} else if (propKey === CHILDREN) {
if (typeof nextProp === 'string') {
// Avoid setting initial textContent when the text is empty. In IE11 setting
// textContent on a <textarea> will cause the placeholder to not
// show within the <textarea> until it has been focused and blurred again.
// https://github.com/facebook/react/issues/6731#issuecomment-254874553
const canSetTextContent = tag !== 'textarea' || nextProp !== '';
if (canSetTextContent) {
setTextContent(domElement, nextProp);
}
} else if (typeof nextProp === 'number') {
setTextContent(domElement, '' + nextProp);
}

So children of string or number is treated as text content of the component

If children that has expressions, it will be an array so won't fall into this branch

} else if (
propKey === SUPPRESS_CONTENT_EDITABLE_WARNING ||
propKey === SUPPRESS_HYDRATION_WARNING
) {
// Noop
} else if (propKey === AUTOFOCUS) {
// We polyfill it separately on the client during commit.
// We could have excluded it in the property list instead of
// adding a special case here, but then it wouldn't be emitted
// on server rendering (but we *do* want to emit it in SSR).
} else if (registrationNameDependencies.hasOwnProperty(propKey)) {
if (nextProp != null) {
if (propKey === 'onScroll') {
listenToNonDelegatedEvent('scroll', domElement);
}
}
} else if (nextProp != null) {
setValueForProperty(domElement, propKey, nextProp, isCustomComponentTag);
}
}
}
export function finalizeInitialChildren(
domElement: Instance,
type: string,
props: Props,
hostContext: HostContext,
): boolean {
setInitialProperties(domElement, type, props);
--------------------
...
}
export function setInitialProperties(
domElement: Element,
tag: string,
rawProps: Object,
rootContainerElement: Element | Document | DocumentFragment,
): void {
...
setInitialDOMProperties(
-----------------------
tag,
domElement,
rootContainerElement,
props,
isCustomComponentTag,
);
...
}
function setInitialDOMProperties(
tag: string,
domElement: Element,
rootContainerElement: Element | Document | DocumentFragment,
nextProps: Object,
isCustomComponentTag: boolean,
): void {
for (const propKey in nextProps) {
if (!nextProps.hasOwnProperty(propKey)) {
continue;
}
const nextProp = nextProps[propKey];
if (propKey === STYLE) {
// Relies on `updateStylesByID` not mutating `styleUpdates`.
setValueForStyles(domElement, nextProp);
} else if (propKey === DANGEROUSLY_SET_INNER_HTML) {
const nextHtml = nextProp ? nextProp[HTML] : undefined;
if (nextHtml != null) {
setInnerHTML(domElement, nextHtml);
}
} else if (propKey === CHILDREN) {
if (typeof nextProp === 'string') {
// Avoid setting initial textContent when the text is empty. In IE11 setting
// textContent on a <textarea> will cause the placeholder to not
// show within the <textarea> until it has been focused and blurred again.
// https://github.com/facebook/react/issues/6731#issuecomment-254874553
const canSetTextContent = tag !== 'textarea' || nextProp !== '';
if (canSetTextContent) {
setTextContent(domElement, nextProp);
}
} else if (typeof nextProp === 'number') {
setTextContent(domElement, '' + nextProp);
}

So children of string or number is treated as text content of the component

If children that has expressions, it will be an array so won't fall into this branch

} else if (
propKey === SUPPRESS_CONTENT_EDITABLE_WARNING ||
propKey === SUPPRESS_HYDRATION_WARNING
) {
// Noop
} else if (propKey === AUTOFOCUS) {
// We polyfill it separately on the client during commit.
// We could have excluded it in the property list instead of
// adding a special case here, but then it wouldn't be emitted
// on server rendering (but we *do* want to emit it in SSR).
} else if (registrationNameDependencies.hasOwnProperty(propKey)) {
if (nextProp != null) {
if (propKey === 'onScroll') {
listenToNonDelegatedEvent('scroll', domElement);
}
}
} else if (nextProp != null) {
setValueForProperty(domElement, propKey, nextProp, isCustomComponentTag);
}
}
}

4. Initial mount in Commit phase

By now:

  1. the workInProgress version of Fiber Tree is finally constructed!
  2. the backing DOM nodes are also created and organized!
  3. flags are set on necessary fibers to help guide the DOM manipulation!

Now it’s time to actually see how React manipulates the DOM.

4.1 commitMutationEffects()

Recall in Overview of React internals we briefly explained Commit phase, we’ll go deeper in commitMutationEffects() which handles the DOM mutation.

export function commitMutationEffects(
root: FiberRoot,
finishedWork: Fiber,

The Fiber Node of HostRoot, holding the newly built Fiber Tree

committedLanes: Lanes,
) {
inProgressLanes = committedLanes;
inProgressRoot = root;
commitMutationEffectsOnFiber(finishedWork, root, committedLanes);
----------------------------
inProgressLanes = null;
inProgressRoot = null;
}
export function commitMutationEffects(
root: FiberRoot,
finishedWork: Fiber,

The Fiber Node of HostRoot, holding the newly built Fiber Tree

committedLanes: Lanes,
) {
inProgressLanes = committedLanes;
inProgressRoot = root;
commitMutationEffectsOnFiber(finishedWork, root, committedLanes);
----------------------------
inProgressLanes = null;
inProgressRoot = null;
}
function commitMutationEffectsOnFiber(
finishedWork: Fiber,
root: FiberRoot,
lanes: Lanes,
) {
const current = finishedWork.alternate;
const flags = finishedWork.flags;
// The effect flag should be checked *after* we refine the type of fiber,
// because the fiber tag is more specific. An exception is any flag related
// to reconciliation, because those can be set on all fiber types.
switch (finishedWork.tag) {
case FunctionComponent:
case ForwardRef:
case MemoComponent:
case SimpleMemoComponent: {
recursivelyTraverseMutationEffects(root, finishedWork, lanes);
----------------------------------

This recursive call makes sure subtree is handled first

commitReconciliationEffects(finishedWork);
---------------------------

ReconciliationEffects means Insertion .etc.

...
return;
}
...
case HostComponent: {
recursivelyTraverseMutationEffects(root, finishedWork, lanes);
commitReconciliationEffects(finishedWork);
...
return;
}
case HostText: {
recursivelyTraverseMutationEffects(root, finishedWork, lanes);
commitReconciliationEffects(finishedWork);
...
return;
}
case HostRoot: {
if (enableFloat && supportsResources) {
prepareToCommitHoistables();
const previousHoistableRoot = currentHoistableRoot;
currentHoistableRoot = getHoistableRoot(root.containerInfo);
recursivelyTraverseMutationEffects(root, finishedWork, lanes);
currentHoistableRoot = previousHoistableRoot;
commitReconciliationEffects(finishedWork);
} else {
recursivelyTraverseMutationEffects(root, finishedWork, lanes);
commitReconciliationEffects(finishedWork);
}
...
return;
}
...
default: {
recursivelyTraverseMutationEffects(root, finishedWork, lanes);
commitReconciliationEffects(finishedWork);
return;
}
}
}
function commitMutationEffectsOnFiber(
finishedWork: Fiber,
root: FiberRoot,
lanes: Lanes,
) {
const current = finishedWork.alternate;
const flags = finishedWork.flags;
// The effect flag should be checked *after* we refine the type of fiber,
// because the fiber tag is more specific. An exception is any flag related
// to reconciliation, because those can be set on all fiber types.
switch (finishedWork.tag) {
case FunctionComponent:
case ForwardRef:
case MemoComponent:
case SimpleMemoComponent: {
recursivelyTraverseMutationEffects(root, finishedWork, lanes);
----------------------------------

This recursive call makes sure subtree is handled first

commitReconciliationEffects(finishedWork);
---------------------------

ReconciliationEffects means Insertion .etc.

...
return;
}
...
case HostComponent: {
recursivelyTraverseMutationEffects(root, finishedWork, lanes);
commitReconciliationEffects(finishedWork);
...
return;
}
case HostText: {
recursivelyTraverseMutationEffects(root, finishedWork, lanes);
commitReconciliationEffects(finishedWork);
...
return;
}
case HostRoot: {
if (enableFloat && supportsResources) {
prepareToCommitHoistables();
const previousHoistableRoot = currentHoistableRoot;
currentHoistableRoot = getHoistableRoot(root.containerInfo);
recursivelyTraverseMutationEffects(root, finishedWork, lanes);
currentHoistableRoot = previousHoistableRoot;
commitReconciliationEffects(finishedWork);
} else {
recursivelyTraverseMutationEffects(root, finishedWork, lanes);
commitReconciliationEffects(finishedWork);
}
...
return;
}
...
default: {
recursivelyTraverseMutationEffects(root, finishedWork, lanes);
commitReconciliationEffects(finishedWork);
return;
}
}
}
function recursivelyTraverseMutationEffects(
root: FiberRoot,
parentFiber: Fiber,
lanes: Lanes,
) {
// Deletions effects can be scheduled on any fiber type. They need to happen
// before the children effects hae fired.
const deletions = parentFiber.deletions;
if (deletions !== null) {
for (let i = 0; i < deletions.length; i++) {
const childToDelete = deletions[i];
try {
commitDeletionEffects(root, parentFiber, childToDelete);
} catch (error) {
captureCommitPhaseError(childToDelete, parentFiber, error);
}
}
}

Deletion are handled differently

const prevDebugFiber = getCurrentDebugFiberInDEV();
if (parentFiber.subtreeFlags & MutationMask) {
let child = parentFiber.child;
while (child !== null) {
setCurrentDebugFiberInDEV(child);
commitMutationEffectsOnFiber(child, root, lanes);
child = child.sibling;
}
}
setCurrentDebugFiberInDEV(prevDebugFiber);
}
function recursivelyTraverseMutationEffects(
root: FiberRoot,
parentFiber: Fiber,
lanes: Lanes,
) {
// Deletions effects can be scheduled on any fiber type. They need to happen
// before the children effects hae fired.
const deletions = parentFiber.deletions;
if (deletions !== null) {
for (let i = 0; i < deletions.length; i++) {
const childToDelete = deletions[i];
try {
commitDeletionEffects(root, parentFiber, childToDelete);
} catch (error) {
captureCommitPhaseError(childToDelete, parentFiber, error);
}
}
}

Deletion are handled differently

const prevDebugFiber = getCurrentDebugFiberInDEV();
if (parentFiber.subtreeFlags & MutationMask) {
let child = parentFiber.child;
while (child !== null) {
setCurrentDebugFiberInDEV(child);
commitMutationEffectsOnFiber(child, root, lanes);
child = child.sibling;
}
}
setCurrentDebugFiberInDEV(prevDebugFiber);
}

4.2 commitReconciliationEffects()

commitReconciliationEffects() handles the Insertion, reordering .etc.

function commitReconciliationEffects(finishedWork: Fiber) {
// Placement effects (insertions, reorders) can be scheduled on any fiber
// type. They needs to happen after the children effects have fired, but
// before the effects on this fiber have fired.
const flags = finishedWork.flags;
if (flags & Placement) {
---------

Yep, the flag are checked here!

try {
commitPlacement(finishedWork);
---------------
} catch (error) {
captureCommitPhaseError(finishedWork, finishedWork.return, error);
}
// Clear the "placement" from effect tag so that we know that this is
// inserted, before any life-cycles like componentDidMount gets called.
// TODO: findDOMNode doesn't rely on this any more but isMounted does
// and isMounted is deprecated anyway so we should be able to kill this.
finishedWork.flags &= ~Placement;
}
...
}
function commitReconciliationEffects(finishedWork: Fiber) {
// Placement effects (insertions, reorders) can be scheduled on any fiber
// type. They needs to happen after the children effects have fired, but
// before the effects on this fiber have fired.
const flags = finishedWork.flags;
if (flags & Placement) {
---------

Yep, the flag are checked here!

try {
commitPlacement(finishedWork);
---------------
} catch (error) {
captureCommitPhaseError(finishedWork, finishedWork.return, error);
}
// Clear the "placement" from effect tag so that we know that this is
// inserted, before any life-cycles like componentDidMount gets called.
// TODO: findDOMNode doesn't rely on this any more but isMounted does
// and isMounted is deprecated anyway so we should be able to kill this.
finishedWork.flags &= ~Placement;
}
...
}

So in our demo , Fiber Node of <App/> actually gets committed.

4.3 commitPlacement()

function commitPlacement(finishedWork: Fiber): void {
...
// Recursively insert all host nodes into the parent.
const parentFiber = getHostParentFiber(finishedWork);
switch (parentFiber.tag) {
---------------

Notice here we are checking parent fiber's type

Because Insertion is done against parent node]

case HostSingleton: {
if (enableHostSingletons && supportsSingletons) {
const parent: Instance = parentFiber.stateNode;
const before = getHostSibling(finishedWork);
// We only have the top Fiber that was inserted but we need to recurse down its
// children to find all the terminal nodes.
insertOrAppendPlacementNode(finishedWork, before, parent);
break;
}
// Fall through
}
case HostComponent: {
-------------

We won't touch this branch in initial mount

const parent: Instance = parentFiber.stateNode;
if (parentFiber.flags & ContentReset) {
// Reset the text content of the parent before doing any insertions
resetTextContent(parent);
// Clear ContentReset from the effect tag
parentFiber.flags &= ~ContentReset;
}
const before = getHostSibling(finishedWork);
// We only have the top Fiber that was inserted but we need to recurse down its
// children to find all the terminal nodes.
insertOrAppendPlacementNode(finishedWork, before, parent);
break;
}
case HostRoot:
--------

The Fiber Node that has Placement flag for initial mount is <App/>

its parentFiber is HostRoot

case HostPortal: {
const parent: Container = parentFiber.stateNode.containerInfo;
---------

the stateNode of HostRoot points to FiberRootNode

const before = getHostSibling(finishedWork);
insertOrAppendPlacementNodeIntoContainer(finishedWork, before, parent);
break;
}
default:
throw new Error(
'Invalid host parent fiber. This error is likely caused by a bug ' +
'in React. Please file an issue.',
);
}
}
function commitPlacement(finishedWork: Fiber): void {
...
// Recursively insert all host nodes into the parent.
const parentFiber = getHostParentFiber(finishedWork);
switch (parentFiber.tag) {
---------------

Notice here we are checking parent fiber's type

Because Insertion is done against parent node]

case HostSingleton: {
if (enableHostSingletons && supportsSingletons) {
const parent: Instance = parentFiber.stateNode;
const before = getHostSibling(finishedWork);
// We only have the top Fiber that was inserted but we need to recurse down its
// children to find all the terminal nodes.
insertOrAppendPlacementNode(finishedWork, before, parent);
break;
}
// Fall through
}
case HostComponent: {
-------------

We won't touch this branch in initial mount

const parent: Instance = parentFiber.stateNode;
if (parentFiber.flags & ContentReset) {
// Reset the text content of the parent before doing any insertions
resetTextContent(parent);
// Clear ContentReset from the effect tag
parentFiber.flags &= ~ContentReset;
}
const before = getHostSibling(finishedWork);
// We only have the top Fiber that was inserted but we need to recurse down its
// children to find all the terminal nodes.
insertOrAppendPlacementNode(finishedWork, before, parent);
break;
}
case HostRoot:
--------

The Fiber Node that has Placement flag for initial mount is <App/>

its parentFiber is HostRoot

case HostPortal: {
const parent: Container = parentFiber.stateNode.containerInfo;
---------

the stateNode of HostRoot points to FiberRootNode

const before = getHostSibling(finishedWork);
insertOrAppendPlacementNodeIntoContainer(finishedWork, before, parent);
break;
}
default:
throw new Error(
'Invalid host parent fiber. This error is likely caused by a bug ' +
'in React. Please file an issue.',
);
}
}

The idea is to insert or append the DOM of finishedWork into parent container, at the right position.

function insertOrAppendPlacementNodeIntoContainer(
node: Fiber,
before: ?Instance,
parent: Container,
): void {
const {tag} = node;
const isHost = tag === HostComponent || tag === HostText;
if (isHost) {
const stateNode = node.stateNode;
if (before) {
insertInContainerBefore(parent, stateNode, before);
} else {
appendChildToContainer(parent, stateNode);
}

if it is DOM elements, just insert it

} else if (
tag === HostPortal ||
(enableHostSingletons && supportsSingletons ? tag === HostSingleton : false)
) {
// If the insertion itself is a portal, then we don't want to traverse
// down its children. Instead, we'll get insertions from each child in
// the portal directly.
// If the insertion is a HostSingleton then it will be placed independently
} else {
const child = node.child;
if (child !== null) {
insertOrAppendPlacementNodeIntoContainer(child, before, parent);
let sibling = child.sibling;
while (sibling !== null) {
insertOrAppendPlacementNodeIntoContainer(sibling, before, parent);
sibling = sibling.sibling;
}
}

For non-DOM elements, recursively handle their children

}
}
function insertOrAppendPlacementNodeIntoContainer(
node: Fiber,
before: ?Instance,
parent: Container,
): void {
const {tag} = node;
const isHost = tag === HostComponent || tag === HostText;
if (isHost) {
const stateNode = node.stateNode;
if (before) {
insertInContainerBefore(parent, stateNode, before);
} else {
appendChildToContainer(parent, stateNode);
}

if it is DOM elements, just insert it

} else if (
tag === HostPortal ||
(enableHostSingletons && supportsSingletons ? tag === HostSingleton : false)
) {
// If the insertion itself is a portal, then we don't want to traverse
// down its children. Instead, we'll get insertions from each child in
// the portal directly.
// If the insertion is a HostSingleton then it will be placed independently
} else {
const child = node.child;
if (child !== null) {
insertOrAppendPlacementNodeIntoContainer(child, before, parent);
let sibling = child.sibling;
while (sibling !== null) {
insertOrAppendPlacementNodeIntoContainer(sibling, before, parent);
sibling = sibling.sibling;
}
}

For non-DOM elements, recursively handle their children

}
}

And this is how the DOM is finally inserted.

5. Summary

Alright, we finally see how the DOM is created and inserted to the container. So for initial mount,

  1. Fiber Tree is lazily created during reconciliation, the backing DOM nodes are created and organized at the same time.
  2. The direct child of HostRoot is marked as Placement.
  3. In commit phase, we just find fiber with Placement. Since its parent is HostRoot, its DOM node is inserted into container.

How React does initial mount(first-time render)

(1/27)

Want to know more about how React works internally?
Check out my series - React Internals Deep Dive!

😳 Would you like to share my post to more people ?    

❮ Prev: How does React work under the hood ? The Overview of React internals

Next: How does React re-render internally?