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.
- 1. Brief introduction on Fiber Architecture
- 2. Initial mount in Trigger phase
- 3. Initial mount in Render phase
- 3.1
performConcurrentWorkOnRoot()
- 3.2
renderRootSync()
- 3.3
performUnitOfWork()
- 3.4
prepareFreshStack()
- 3.5
updateHostRoot()
- 3.6
reconcileChildren()
- 3.7
reconcileChildFibers()
vsmountChildFibers()
- 3.8
reconcileSingleElement()
- 3.9
placeSingleChild()
- 3.10
mountIndeterminateComponent()
- 3.11
updateHostComponent()
- 3.12
updateHostText()
- 3.13 DOM nodes are created offscreen in
completeWork()
- 3.1
- 4. Initial mount in Commit phase
- 5.Summary
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:
tag
: FiberNode has many sub type differentiated bytag
. For example, FunctionComponent, HostRoot, ContextConsumer,MemoComponent,SuspenseComponent .etcstateNode
: it points to other backing data, forHostComponent
,stateNode
points to the actual backing DOM node.child
,sibling
andreturn
: these together form a tree-like structure.elementType
which is the component function or intrinsic HTML tag we provide.flags
: to indicate the updates to apply in Commit phase.subtreeFlags
is for its subtree.lanes
: to indicate the priority of pending updates.childLanes
if for its subtree.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 functionsconst 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 functionsconst 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 itperformUnitOfWork(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 itperformUnitOfWork(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 loopthat keeps running
completeUnitOfWork()
on workInProgressSo 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 loopthat keeps running
completeUnitOfWork()
on workInProgressSo 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 = falseotherwise 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 = falseotherwise 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 HostRootworkInProgress = 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 HostRootworkInProgress = 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 typesif (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 typesif (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$ElementTypekey: 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$ElementTypekey: 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 instanceworkInProgress.tag = ClassComponent;Once rendered, it is no long IndeterminateComponent
...} else {// Proceed under the assumption that this is a function componentworkInProgress.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 usedreturn 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 instanceworkInProgress.tag = ClassComponent;Once rendered, it is no long IndeterminateComponent
...} else {// Proceed under the assumption that this is a function componentworkInProgress.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 usedreturn 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);For hydration, refer to How basic hydration works internally in React
}const type = workInProgress.type;const nextProps = workInProgress.pendingProps;
pendingProps
holds thechildren
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);For hydration, refer to How basic hydration works internally in React
}const type = workInProgress.type;const nextProps = workInProgress.pendingProps;
pendingProps
holds thechildren
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 callbackmarkRef(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 callbackmarkRef(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-254874553const 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 componentIf 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-254874553const 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 componentIf 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:
- the workInProgress version of Fiber Tree is finally constructed!
- the backing DOM nodes are also created and organized!
- 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 insertionsresetTextContent(parent);// Clear ContentReset from the effect tagparentFiber.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 insertionsresetTextContent(parent);// Clear ContentReset from the effect tagparentFiber.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,
- Fiber Tree is lazily created during reconciliation, the backing DOM nodes are created and organized at the same time.
- The direct child of
HostRoot
is marked asPlacement
. - In commit phase, we just find fiber with
Placement
. Since its parent is HostRoot, its DOM node is inserted into container.
Want to know more about how React works internally?
Check out my series - React Internals Deep Dive!