How does React re-render internally?
I’ve explained how React does the initial mount and creates the full DOM from scratch. For re-renders after initial mount, React tries to reuse the DOM as much as possible through the process of reconciliation. In this episode, we’ll figure out what actually happens when React re-renders after clicking the button in demo below.
- 1. Re-render in Trigger phase
- 2. Re-render in Render phase.
- 2.1 Basic rendering logic is the same as initial mount.
- 2.2 React reuses redundant Fiber Nodes before creating new ones
- 2.3 The Update branch in
beginWork()
- 2.4 Bailout logic inside
attemptEarlyBailoutIfNoScheduledUpdate()
- 2.5
memoizedProps
vspendingProps
. - 2.6
updateFunctionComponent()
re-render function components and reconcile children. - 2.7
reconcileSingleElement()
. - 2.8 Once a component is re-rendered, their subtree is re-rendered by default.
- 2.9
updateHostComponent()
. - 2.10
reconcileChildrenArray()
creates and deletes fibers as needed. - 2.11
placeChild()
anddeleteChild()
marks fiber with flags - 2.12
updateHostText()
- 2.13
completeWork()
marks the update of HostComponent and creates DOM nodes if necessary
- 3. Re-render in Commit Phase
- 4. Summary
1. Re-render in Trigger phase
React constructs the Fiber Tree and DOM tree in initial mount, when it’s done we have two trees as below.
1.1 lanes
and childLanes
Lane is the priority of pending work. For a Fiber Node, it has:
lanes
=> for the pending work of itselfchildLanes
=> for the pending work of its subtree
When the button is clicked, setState()
is called:
- the path from root to the target fiber
will be marked with
lanes
andchildLanes
to indicate places that need to be checked in next render. - an update is scheduled by
scheduleUpdateOnFiber()
, in which eventuallyensureRootIsScheduled()
is called andperformConcurrentWorkOnRoot()
is scheduled in Scheduler. This is quite similar to the initial mount.
One important thing to keep in mind is that priority of event determines the priority
of the update. For click
event, it is DiscreteEventPriority
, which maps to SyncLane
- high priority.
useState()
works, refer to How does useState() work internally in React. We’ll skip the details here but eventually we get following Fiber Tree to work on.
2. Re-render in Render phase.
2.1 Basic rendering logic is the same as initial mount.
Since under click
event, the rendering lane is SyncLane which is blocking lane.
So same as initial mount, concurrent mode is still not activated inside performConcurrentWorkOnRoot()
.
Below is the code that summarize the whole process.
do {try {workLoopSync();break;} catch (thrownValue) {handleError(root, thrownValue);}} while (true);function workLoopSync() {while (workInProgress !== null) {performUnitOfWork(workInProgress);}}function performUnitOfWork(unitOfWork: Fiber): void {const current = unitOfWork.alternate;next = beginWork(current, unitOfWork, subtreeRenderLanes);---------unitOfWork.memoizedProps = unitOfWork.pendingProps;--------------------------------------------------This line is important, we'll cover it in 2.5 memoizedProps vs pendingProps
if (next === null) {// If this doesn't spawn new work, complete the current work.completeUnitOfWork(unitOfWork);} else {workInProgress = next;}}
do {try {workLoopSync();break;} catch (thrownValue) {handleError(root, thrownValue);}} while (true);function workLoopSync() {while (workInProgress !== null) {performUnitOfWork(workInProgress);}}function performUnitOfWork(unitOfWork: Fiber): void {const current = unitOfWork.alternate;next = beginWork(current, unitOfWork, subtreeRenderLanes);---------unitOfWork.memoizedProps = unitOfWork.pendingProps;--------------------------------------------------This line is important, we'll cover it in 2.5 memoizedProps vs pendingProps
if (next === null) {// If this doesn't spawn new work, complete the current work.completeUnitOfWork(unitOfWork);} else {workInProgress = next;}}
Refer to my previous episode for better explanation. Here just remember that React traverse the Fiber Tree and updates the fiber if necessary.
2.2 React reuses redundant Fiber Nodes before creating new ones
In initial mount we see that Fibers are created from scratch. But actually React tries to reuse the Fiber Nodes first.
export function createWorkInProgress(current: Fiber, pendingProps: any): Fiber {let workInProgress = current.alternate;-----------------curernt is the current version
its alternate is the version before it
if (workInProgress === null) {-----------------------------if we have to create from scratch
// We use a double buffering pooling technique because we know that we'll// only ever need at most two versions of a tree. We pool the "other" unused// node that we're free to reuse. This is lazily created to avoid allocating// extra objects for things that are never updated. It also allow us to// reclaim the extra memory if needed.workInProgress = createFiber(current.tag,pendingProps,------------current.key,current.mode,);...workInProgress.alternate = current;current.alternate = workInProgress;} else {--------if we can reuse the previous version
workInProgress.pendingProps = pendingProps;------------------------------------------Since we can reuse, we don't need to create Fiber Node
But just reusing it by updating the properties
// Needed because Blocks store data on type.workInProgress.type = current.type;// We already have an alternate.// Reset the effect tag.workInProgress.flags = NoFlags;// The effects are no longer valid.workInProgress.subtreeFlags = NoFlags;workInProgress.deletions = null;}// Reset all effects except static ones.// Static effects are not specific to a render.workInProgress.flags = current.flags & StaticMask;workInProgress.childLanes = current.childLanes;workInProgress.lanes = current.lanes;lanes and childLanes are copied
workInProgress.child = current.child;workInProgress.memoizedProps = current.memoizedProps;workInProgress.memoizedState = current.memoizedState;workInProgress.updateQueue = current.updateQueue;...return workInProgress;}
export function createWorkInProgress(current: Fiber, pendingProps: any): Fiber {let workInProgress = current.alternate;-----------------curernt is the current version
its alternate is the version before it
if (workInProgress === null) {-----------------------------if we have to create from scratch
// We use a double buffering pooling technique because we know that we'll// only ever need at most two versions of a tree. We pool the "other" unused// node that we're free to reuse. This is lazily created to avoid allocating// extra objects for things that are never updated. It also allow us to// reclaim the extra memory if needed.workInProgress = createFiber(current.tag,pendingProps,------------current.key,current.mode,);...workInProgress.alternate = current;current.alternate = workInProgress;} else {--------if we can reuse the previous version
workInProgress.pendingProps = pendingProps;------------------------------------------Since we can reuse, we don't need to create Fiber Node
But just reusing it by updating the properties
// Needed because Blocks store data on type.workInProgress.type = current.type;// We already have an alternate.// Reset the effect tag.workInProgress.flags = NoFlags;// The effects are no longer valid.workInProgress.subtreeFlags = NoFlags;workInProgress.deletions = null;}// Reset all effects except static ones.// Static effects are not specific to a render.workInProgress.flags = current.flags & StaticMask;workInProgress.childLanes = current.childLanes;workInProgress.lanes = current.lanes;lanes and childLanes are copied
workInProgress.child = current.child;workInProgress.memoizedProps = current.memoizedProps;workInProgress.memoizedState = current.memoizedState;workInProgress.updateQueue = current.updateQueue;...return workInProgress;}
Since FiberRootNode points to the current Fiber Tree through current
, every
Fiber Node that is not on the current tree could be reused.
In our re-render process, the redundant HostRoot
will be reused in 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 we’ll start the re-rendering with following head.
Let’s give it some color
2.3 The Update branch in beginWork()
In beginWork()
, there is an important branch that handles updates, which we didn’t
touch in the episode of initial mount.
function beginWork(current: Fiber | null,current is the current version, which is being painted
workInProgress: Fiber,workInProgress is the new version, which is to be painted
renderLanes: Lanes,): Fiber | null {if (current !== null) {---------------------If current is not null, it means NOT initial mount,
it has previous version of Fiber Node and also DOM nodes
if it is HostComponent. Thus React is able to optimize by possibly
avoiding going deeper in the subtree - bailout!
const oldProps = current.memoizedProps;const newProps = workInProgress.pendingProps;if (oldProps !== newProps ||---------------------Here it is using
===
not shallow equalwhich leads to important behavior of React rendering
hasLegacyContextChanged() ||// Force a re-render if the implementation changed due to hot reload:(__DEV__ ? workInProgress.type !== current.type : false)) {// If props or context changed, mark the fiber as having performed work.// This may be unset if the props are determined to be equal later (memo).didReceiveUpdate = true;} else {// Neither props nor legacy context changes. Check if there's a pending// update or context change.const hasScheduledUpdateOrContext = checkScheduledUpdateOrContext(-----------------------------This checks the
lanes
on the fiberscurrent,renderLanes,);if (!hasScheduledUpdateOrContext &&// If this is the second pass of an error or suspense boundary, there// may not be work scheduled on `current`, so we check for this flag.(workInProgress.flags & DidCapture) === NoFlags) {// No pending updates or context. Bail out now.didReceiveUpdate = false;return attemptEarlyBailoutIfNoScheduledUpdate(---------------------------------------------If there is no update on this fiber, React tries to bailout
but only if there are no props or context changes
current,workInProgress,renderLanes,);}...}} else {didReceiveUpdate = false;This is mount branch we've already covered before
...}workInProgress.lanes = NoLanes;switch (workInProgress.tag) {case IndeterminateComponent: {return mountIndeterminateComponent(current,workInProgress,workInProgress.type,renderLanes,);}case FunctionComponent: {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:return updateHostRoot(current, workInProgress, renderLanes);--------------case HostComponent:return updateHostComponent(current, workInProgress, renderLanes);-------------------case HostText:return updateHostText(current, workInProgress);--------------...}}
function beginWork(current: Fiber | null,current is the current version, which is being painted
workInProgress: Fiber,workInProgress is the new version, which is to be painted
renderLanes: Lanes,): Fiber | null {if (current !== null) {---------------------If current is not null, it means NOT initial mount,
it has previous version of Fiber Node and also DOM nodes
if it is HostComponent. Thus React is able to optimize by possibly
avoiding going deeper in the subtree - bailout!
const oldProps = current.memoizedProps;const newProps = workInProgress.pendingProps;if (oldProps !== newProps ||---------------------Here it is using
===
not shallow equalwhich leads to important behavior of React rendering
hasLegacyContextChanged() ||// Force a re-render if the implementation changed due to hot reload:(__DEV__ ? workInProgress.type !== current.type : false)) {// If props or context changed, mark the fiber as having performed work.// This may be unset if the props are determined to be equal later (memo).didReceiveUpdate = true;} else {// Neither props nor legacy context changes. Check if there's a pending// update or context change.const hasScheduledUpdateOrContext = checkScheduledUpdateOrContext(-----------------------------This checks the
lanes
on the fiberscurrent,renderLanes,);if (!hasScheduledUpdateOrContext &&// If this is the second pass of an error or suspense boundary, there// may not be work scheduled on `current`, so we check for this flag.(workInProgress.flags & DidCapture) === NoFlags) {// No pending updates or context. Bail out now.didReceiveUpdate = false;return attemptEarlyBailoutIfNoScheduledUpdate(---------------------------------------------If there is no update on this fiber, React tries to bailout
but only if there are no props or context changes
current,workInProgress,renderLanes,);}...}} else {didReceiveUpdate = false;This is mount branch we've already covered before
...}workInProgress.lanes = NoLanes;switch (workInProgress.tag) {case IndeterminateComponent: {return mountIndeterminateComponent(current,workInProgress,workInProgress.type,renderLanes,);}case FunctionComponent: {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:return updateHostRoot(current, workInProgress, renderLanes);--------------case HostComponent:return updateHostComponent(current, workInProgress, renderLanes);-------------------case HostText:return updateHostText(current, workInProgress);--------------...}}
2.4 Bailout logic inside attemptEarlyBailoutIfNoScheduledUpdate()
Per its name, this function tries to stop the rendering sooner if unnecessary.
function attemptEarlyBailoutIfNoScheduledUpdate(current: Fiber,workInProgress: Fiber,renderLanes: Lanes,) {// This fiber does not have any pending work. Bailout without entering// the begin phase. There's still some bookkeeping we that needs to be done// in this optimized path, mostly pushing stuff onto the stack.switch (workInProgress.tag) {case HostRoot:pushHostRootContext(workInProgress);const root: FiberRoot = workInProgress.stateNode;pushRootTransition(workInProgress, root, renderLanes);if (enableCache) {const cache: Cache = current.memoizedState.cache;pushCacheProvider(workInProgress, cache);}resetHydrationState();break;case HostComponent:pushHostContext(workInProgress);break;...}return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);----------------------------}
function attemptEarlyBailoutIfNoScheduledUpdate(current: Fiber,workInProgress: Fiber,renderLanes: Lanes,) {// This fiber does not have any pending work. Bailout without entering// the begin phase. There's still some bookkeeping we that needs to be done// in this optimized path, mostly pushing stuff onto the stack.switch (workInProgress.tag) {case HostRoot:pushHostRootContext(workInProgress);const root: FiberRoot = workInProgress.stateNode;pushRootTransition(workInProgress, root, renderLanes);if (enableCache) {const cache: Cache = current.memoizedState.cache;pushCacheProvider(workInProgress, cache);}resetHydrationState();break;case HostComponent:pushHostContext(workInProgress);break;...}return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);----------------------------}
function bailoutOnAlreadyFinishedWork(current: Fiber | null,workInProgress: Fiber,renderLanes: Lanes,): Fiber | null {if (current !== null) {// Reuse previous dependenciesworkInProgress.dependencies = current.dependencies;}// Check if the children have any pending work.if (!includesSomeLane(renderLanes, workInProgress.childLanes)) {---------------------------------------------------------------Here we see
childLanes
are checked// The children don't have any work either. We can skip them.// TODO: Once we add back resuming, we should check if the children are// a work-in-progress set. If so, we need to transfer their effects.if (enableLazyContextPropagation && current !== null) {// Before bailing out, check if there are any context changes in// the children.lazilyPropagateParentContextChanges(current, workInProgress, renderLanes);if (!includesSomeLane(renderLanes, workInProgress.childLanes)) {return null;}} else {return null;-----------So if there is no update on fiber itself and its subtree
Then of course, we can just stop going deeper in the tree
by returning null
}}// This fiber doesn't have work, but its subtree does. Clone the child// fibers and continue.cloneChildFibers(current, workInProgress);Though its name is clone, it actually either creates new children nodes
or reusing the previous nodes
return workInProgress.child;We just return the child directly, React process it as next fiber
For more, refer to How does React traverse Fiber tree internally
}export function cloneChildFibers(current: Fiber | null,workInProgress: Fiber,): void {if (current !== null && workInProgress.child !== current.child) {--------------------------------------------------------------throw new Error('Resuming work not yet implemented.');}if (workInProgress.child === null) {return;}let currentChild = workInProgress.child;let newChild = createWorkInProgress(currentChild, currentChild.pendingProps);------------So in cloneChildFibers(), child fibers are created from its previous version
but with new pendingProps, which is set during reconciliation
workInProgress.child = newChild;newChild.return = workInProgress;while (currentChild.sibling !== null) {currentChild = currentChild.sibling;newChild = newChild.sibling = createWorkInProgress(currentChild,currentChild.pendingProps,);newChild.return = workInProgress;}newChild.sibling = null;}
function bailoutOnAlreadyFinishedWork(current: Fiber | null,workInProgress: Fiber,renderLanes: Lanes,): Fiber | null {if (current !== null) {// Reuse previous dependenciesworkInProgress.dependencies = current.dependencies;}// Check if the children have any pending work.if (!includesSomeLane(renderLanes, workInProgress.childLanes)) {---------------------------------------------------------------Here we see
childLanes
are checked// The children don't have any work either. We can skip them.// TODO: Once we add back resuming, we should check if the children are// a work-in-progress set. If so, we need to transfer their effects.if (enableLazyContextPropagation && current !== null) {// Before bailing out, check if there are any context changes in// the children.lazilyPropagateParentContextChanges(current, workInProgress, renderLanes);if (!includesSomeLane(renderLanes, workInProgress.childLanes)) {return null;}} else {return null;-----------So if there is no update on fiber itself and its subtree
Then of course, we can just stop going deeper in the tree
by returning null
}}// This fiber doesn't have work, but its subtree does. Clone the child// fibers and continue.cloneChildFibers(current, workInProgress);Though its name is clone, it actually either creates new children nodes
or reusing the previous nodes
return workInProgress.child;We just return the child directly, React process it as next fiber
For more, refer to How does React traverse Fiber tree internally
}export function cloneChildFibers(current: Fiber | null,workInProgress: Fiber,): void {if (current !== null && workInProgress.child !== current.child) {--------------------------------------------------------------throw new Error('Resuming work not yet implemented.');}if (workInProgress.child === null) {return;}let currentChild = workInProgress.child;let newChild = createWorkInProgress(currentChild, currentChild.pendingProps);------------So in cloneChildFibers(), child fibers are created from its previous version
but with new pendingProps, which is set during reconciliation
workInProgress.child = newChild;newChild.return = workInProgress;while (currentChild.sibling !== null) {currentChild = currentChild.sibling;newChild = newChild.sibling = createWorkInProgress(currentChild,currentChild.pendingProps,);newChild.return = workInProgress;}newChild.sibling = null;}
Let’s summarize the bailout process
- if a fiber doesn’t have props/context changes and it doesn’t have pending work (empty
lanes
)- if its children don’t have pending work(empty
childLanes
), bailout happens and React doesn’t go deeper in the tree. - otherwise React goes straight to its children, without re-rendering this fiber
- if its children don’t have pending work(empty
- otherwise, React tries to re-render it first and then goes to its children.
2.5 memoizedProps
vs pendingProps
.
In beginWork()
, workInProgress
is diffed against current
. For the props,
it is workInProgress.pendingProps
against current.memoizedProps
. We can
think of memoizedProps
as current props while pendingProps
as next version.
React creates a new Fiber Tree in Render phase and then diff against current
Fiber Tree. We can see that pendingProps
is actually a parameter for workInProgress
creation.
export function createWorkInProgress(current: Fiber, pendingProps: any): Fiber {let workInProgress = current.alternate;-----------------current is the current version
its alternate is the version before it
if (workInProgress === null) {-----------------------------if we have to create from scratch
// We use a double buffering pooling technique because we know that we'll// only ever need at most two versions of a tree. We pool the "other" unused// node that we're free to reuse. This is lazily created to avoid allocating// extra objects for things that are never updated. It also allow us to// reclaim the extra memory if needed.workInProgress = createFiber(current.tag,pendingProps,------------current.key,current.mode,);...workInProgress.alternate = current;current.alternate = workInProgress;} else {--------if we can reuse the previous version
workInProgress.pendingProps = pendingProps;------------------------------------------Since we can reuse, we don't need to create Fiber Node
But just reusing it by updating the necessary properties
// Needed because Blocks store data on type.workInProgress.type = current.type;// We already have an alternate.// Reset the effect tag.workInProgress.flags = NoFlags;// The effects are no longer valid.workInProgress.subtreeFlags = NoFlags;workInProgress.deletions = null;}// Reset all effects except static ones.// Static effects are not specific to a render.workInProgress.flags = current.flags & StaticMask;workInProgress.childLanes = current.childLanes;workInProgress.lanes = current.lanes;workInProgress.child = current.child;workInProgress.memoizedProps = current.memoizedProps;workInProgress.memoizedState = current.memoizedState;workInProgress.updateQueue = current.updateQueue;// Clone the dependencies object. This is mutated during the render phase, so// it cannot be shared with the current fiber.const currentDependencies = current.dependencies;workInProgress.dependencies =currentDependencies === null? null: {lanes: currentDependencies.lanes,firstContext: currentDependencies.firstContext,};// These will be overridden during the parent's reconciliationworkInProgress.sibling = current.sibling;workInProgress.index = current.index;workInProgress.ref = current.ref;workInProgress.refCleanup = current.refCleanup;return workInProgress;}
export function createWorkInProgress(current: Fiber, pendingProps: any): Fiber {let workInProgress = current.alternate;-----------------current is the current version
its alternate is the version before it
if (workInProgress === null) {-----------------------------if we have to create from scratch
// We use a double buffering pooling technique because we know that we'll// only ever need at most two versions of a tree. We pool the "other" unused// node that we're free to reuse. This is lazily created to avoid allocating// extra objects for things that are never updated. It also allow us to// reclaim the extra memory if needed.workInProgress = createFiber(current.tag,pendingProps,------------current.key,current.mode,);...workInProgress.alternate = current;current.alternate = workInProgress;} else {--------if we can reuse the previous version
workInProgress.pendingProps = pendingProps;------------------------------------------Since we can reuse, we don't need to create Fiber Node
But just reusing it by updating the necessary properties
// Needed because Blocks store data on type.workInProgress.type = current.type;// We already have an alternate.// Reset the effect tag.workInProgress.flags = NoFlags;// The effects are no longer valid.workInProgress.subtreeFlags = NoFlags;workInProgress.deletions = null;}// Reset all effects except static ones.// Static effects are not specific to a render.workInProgress.flags = current.flags & StaticMask;workInProgress.childLanes = current.childLanes;workInProgress.lanes = current.lanes;workInProgress.child = current.child;workInProgress.memoizedProps = current.memoizedProps;workInProgress.memoizedState = current.memoizedState;workInProgress.updateQueue = current.updateQueue;// Clone the dependencies object. This is mutated during the render phase, so// it cannot be shared with the current fiber.const currentDependencies = current.dependencies;workInProgress.dependencies =currentDependencies === null? null: {lanes: currentDependencies.lanes,firstContext: currentDependencies.firstContext,};// These will be overridden during the parent's reconciliationworkInProgress.sibling = current.sibling;workInProgress.index = current.index;workInProgress.ref = current.ref;workInProgress.refCleanup = current.refCleanup;return workInProgress;}
In fact, the root FiberNode constructor has pendingProps
as parameter.
function createFiber(tag: WorkTag,pendingProps: mixed,--------------------key: null | string,mode: TypeOfMode,): Fiber {// $FlowFixMe[invalid-constructor]: the shapes are exact here but Flow doesn't like constructorsreturn new FiberNode(tag, pendingProps, key, mode);}function FiberNode(this: $FlowFixMe,tag: WorkTag,pendingProps: mixed,--------------------key: null | string,mode: TypeOfMode,) {....}
function createFiber(tag: WorkTag,pendingProps: mixed,--------------------key: null | string,mode: TypeOfMode,): Fiber {// $FlowFixMe[invalid-constructor]: the shapes are exact here but Flow doesn't like constructorsreturn new FiberNode(tag, pendingProps, key, mode);}function FiberNode(this: $FlowFixMe,tag: WorkTag,pendingProps: mixed,--------------------key: null | string,mode: TypeOfMode,) {....}
Which makes sense, creating Fiber Node is the very first step. It needs to be worked on later.
And memoizedProps
is set with pendingProps
when re-rendering for a fiber
is done, which is inside performUnitOfWork()
.
function performUnitOfWork(unitOfWork: Fiber): void {const current = unitOfWork.alternate;setCurrentDebugFiberInDEV(unitOfWork);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;---------------------------------------------------The memoizedProps is updated after work is done
if (next === null) {// If this doesn't spawn new work, complete the current work.completeUnitOfWork(unitOfWork);} else {workInProgress = next;}ReactCurrentOwner.current = null;}
function performUnitOfWork(unitOfWork: Fiber): void {const current = unitOfWork.alternate;setCurrentDebugFiberInDEV(unitOfWork);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;---------------------------------------------------The memoizedProps is updated after work is done
if (next === null) {// If this doesn't spawn new work, complete the current work.completeUnitOfWork(unitOfWork);} else {workInProgress = next;}ReactCurrentOwner.current = null;}
Let’s take a look at our demo now.
- React works on HostRoot(lanes: 0, childLanes: 1). HostRoot doesn’t have props and
memoizedProps
andpendingProps
are both null, so React goes straight to its child, which is the clonedApp
. - React works on
<App/>
(lanes: 0, childLanes: 1). App component is not re-rendered somemoizedProps
andpendingProps
are the same, so React goes straight to its child, which is the cloneddiv
. - React works on
<div/>
(lanes: 0, childLanes: 1). It gets its children from App, but App is not re-run so none of its children(<Link>
,<br/>
and<Component/>
) is changed, thus again React goes straight to<Link/>
. - React works on
<Link/>
(lanes: 0, childLanes: 0). This time React doesn’t even need to go deeper, so it stops here and goes to its sibling -<br/>
. - React works on
<br/>
(lanes: 0, childLanes: 0), bailout happens again, React goes to<Component/>
Now something is a bit different. <Component/>
has lanes
of 1
meaning React has to
re-render and reconcile its children, which is done by updateFunctionComponent(current, workInProgress)
.
So far we get following state.
2.6 updateFunctionComponent()
re-renders function components and reconcile children.
function updateFunctionComponent(current,workInProgress,Component,nextProps: any,renderLanes,) {let context;if (!disableLegacyContext) {const unmaskedContext = getUnmaskedContext(workInProgress, Component, true);context = getMaskedContext(workInProgress, unmaskedContext);}let nextChildren;let hasId;prepareToReadContext(workInProgress, renderLanes);nextChildren = renderWithHooks(current,workInProgress,Component,nextProps,context,renderLanes,);Here it means Component is run to generate new children
hasId = checkDidRenderIdHook();if (enableSchedulingProfiler) {markComponentRenderStopped();}if (current !== null && !didReceiveUpdate) {bailoutHooks(current, workInProgress, renderLanes);return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);}// React DevTools reads this flag.workInProgress.flags |= PerformedWork;reconcileChildren(current, workInProgress, nextChildren, renderLanes);----------------------------------------------------------------------We pass down nextChildren, and reconcileChildren() is called
return workInProgress.child;}
function updateFunctionComponent(current,workInProgress,Component,nextProps: any,renderLanes,) {let context;if (!disableLegacyContext) {const unmaskedContext = getUnmaskedContext(workInProgress, Component, true);context = getMaskedContext(workInProgress, unmaskedContext);}let nextChildren;let hasId;prepareToReadContext(workInProgress, renderLanes);nextChildren = renderWithHooks(current,workInProgress,Component,nextProps,context,renderLanes,);Here it means Component is run to generate new children
hasId = checkDidRenderIdHook();if (enableSchedulingProfiler) {markComponentRenderStopped();}if (current !== null && !didReceiveUpdate) {bailoutHooks(current, workInProgress, renderLanes);return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);}// React DevTools reads this flag.workInProgress.flags |= PerformedWork;reconcileChildren(current, workInProgress, nextChildren, renderLanes);----------------------------------------------------------------------We pass down nextChildren, and reconcileChildren() is called
return workInProgress.child;}
We’ve met reconcileChildren()
in how React does the initial mount.
It internally has a few variations depending on the type of children. We’ll focus on 3 of them.
Remember all they do is create new child fibers but also tries to reuse the existing fibers.
function reconcileChildFibersImpl(returnFiber: Fiber,currentFirstChild: Fiber | null,newChild: any,lanes: Lanes,): Fiber | null {...// Handle object typesif (typeof newChild === 'object' && newChild !== null) {switch (newChild.$$typeof) {case REACT_ELEMENT_TYPE:return placeSingleChild(reconcileSingleElement(If just a single child
returnFiber,currentFirstChild,newChild,lanes,),);case REACT_PORTAL_TYPE:...case REACT_LAZY_TYPE:...}if (isArray(newChild)) {return reconcileChildrenArray(returnFiber,currentFirstChild,newChild,lanes,);If children is an array of elements
}...}if ((typeof newChild === 'string' && newChild !== '') ||typeof newChild === 'number') {return placeSingleChild(reconcileSingleTextNode(returnFiber,currentFirstChild,'' + newChild,lanes,),if children is text
);}// Remaining cases are all treated as empty.return deleteRemainingChildren(returnFiber, currentFirstChild);-----------------------}
function reconcileChildFibersImpl(returnFiber: Fiber,currentFirstChild: Fiber | null,newChild: any,lanes: Lanes,): Fiber | null {...// Handle object typesif (typeof newChild === 'object' && newChild !== null) {switch (newChild.$$typeof) {case REACT_ELEMENT_TYPE:return placeSingleChild(reconcileSingleElement(If just a single child
returnFiber,currentFirstChild,newChild,lanes,),);case REACT_PORTAL_TYPE:...case REACT_LAZY_TYPE:...}if (isArray(newChild)) {return reconcileChildrenArray(returnFiber,currentFirstChild,newChild,lanes,);If children is an array of elements
}...}if ((typeof newChild === 'string' && newChild !== '') ||typeof newChild === 'number') {return placeSingleChild(reconcileSingleTextNode(returnFiber,currentFirstChild,'' + newChild,lanes,),if children is text
);}// Remaining cases are all treated as empty.return deleteRemainingChildren(returnFiber, currentFirstChild);-----------------------}
For <Component/>
, it returns a single div
. So we’ll go to reconcileSingleElement()
.
2.7 reconcileSingleElement()
.
function reconcileSingleElement(returnFiber: Fiber,currentFirstChild: Fiber | null,element: ReactElement,-------Here it is the return value of Component(), the element of
<div/>
lanes: Lanes,): Fiber {const key = element.key;let child = currentFirstChild;while (child !== null) {// 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) {...} else {if (child.elementType === elementType ||---------------------------------If types are the same, we can reuse
Otherwise just deleteChild()
// 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);--------Try to use the existing fiber with new props
element.props is the props of
<div/>
existing.ref = coerceRef(returnFiber, child, element);existing.return = returnFiber;return existing;}}// Didn't match.deleteRemainingChildren(returnFiber, child);break;} else {deleteChild(returnFiber, child);}child = child.sibling;}if (element.type === REACT_FRAGMENT_TYPE) {...} else {const created = createFiberFromElement(element, returnFiber.mode, lanes);created.ref = coerceRef(returnFiber, currentFirstChild, element);created.return = returnFiber;return created;}}
function reconcileSingleElement(returnFiber: Fiber,currentFirstChild: Fiber | null,element: ReactElement,-------Here it is the return value of Component(), the element of
<div/>
lanes: Lanes,): Fiber {const key = element.key;let child = currentFirstChild;while (child !== null) {// 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) {...} else {if (child.elementType === elementType ||---------------------------------If types are the same, we can reuse
Otherwise just deleteChild()
// 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);--------Try to use the existing fiber with new props
element.props is the props of
<div/>
existing.ref = coerceRef(returnFiber, child, element);existing.return = returnFiber;return existing;}}// Didn't match.deleteRemainingChildren(returnFiber, child);break;} else {deleteChild(returnFiber, child);}child = child.sibling;}if (element.type === REACT_FRAGMENT_TYPE) {...} else {const created = createFiberFromElement(element, returnFiber.mode, lanes);created.ref = coerceRef(returnFiber, currentFirstChild, element);created.return = returnFiber;return created;}}
And in useFiber
, React creates or reuse the previous version.
As mentioned before, the pendingProps
(which contains children) will be set.
function useFiber(fiber: Fiber, pendingProps: mixed): Fiber {// We currently set sibling to null and index to 0 here because it is easy// to forget to do before returning it. E.g. for the single child case.const clone = createWorkInProgress(fiber, pendingProps);-----------------------------------------clone.index = 0;clone.sibling = null;return clone;}
function useFiber(fiber: Fiber, pendingProps: mixed): Fiber {// We currently set sibling to null and index to 0 here because it is easy// to forget to do before returning it. E.g. for the single child case.const clone = createWorkInProgress(fiber, pendingProps);-----------------------------------------clone.index = 0;clone.sibling = null;return clone;}
So after Component is re-rendered, React goes to its child which is the new <div/>
,
its current version has both empty lanes
and childLanes
.
2.8 Once a component is re-rendered, their subtree is re-rendered by default.
Since <div/>
and its children don’t have scheduled work, you might think bailout
happens, but it doesn’t.
Remember there is a check of memoizedProps
and pendingProps
in beginWork()
.
const oldProps = current.memoizedProps;const newProps = workInProgress.pendingProps;if (oldProps !== newProps ||---------------------Here it is using
===
not shallow equalhasLegacyContextChanged() ||// Force a re-render if the implementation changed due to hot reload:(__DEV__ ? workInProgress.type !== current.type : false)) {// If props or context changed, mark the fiber as having performed work.// This may be unset if the props are determined to be equal later (memo).didReceiveUpdate = true;}
const oldProps = current.memoizedProps;const newProps = workInProgress.pendingProps;if (oldProps !== newProps ||---------------------Here it is using
===
not shallow equalhasLegacyContextChanged() ||// Force a re-render if the implementation changed due to hot reload:(__DEV__ ? workInProgress.type !== current.type : false)) {// If props or context changed, mark the fiber as having performed work.// This may be unset if the props are determined to be equal later (memo).didReceiveUpdate = true;}
Notice that shallow equal is not used in comparing the props,
while every time a component is rendered, it generates a brand new object
containing React elements, so pendingProps
will be newly created every time.
For <div/>
, once Component()
is run, it always gets new props so bailout won’t happen at all.
Thus React goes to update branch - updateHostComponent()
.
2.9 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;const prevProps = current !== null ? current.memoizedProps : null;let nextChildren = nextProps.children;const isDirectTextChild = shouldSetTextContent(type, nextProps);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;const prevProps = current !== null ? current.memoizedProps : null;let nextChildren = nextProps.children;const isDirectTextChild = shouldSetTextContent(type, nextProps);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;}
nextChildren
here is:
js
[{$$typeof: Symbol(react.element), type: 'button'}," (",{$$typeof: Symbol(react.element), type: 'b'},")"]
js
[{$$typeof: Symbol(react.element), type: 'button'}," (",{$$typeof: Symbol(react.element), type: 'b'},")"]
So internally React reconciles it with reconcileChildrenArray()
.
And memoizedProps
of current
is.
js
[{$$typeof: Symbol(react.element), type: 'button'}," (",{$$typeof: Symbol(react.element), type: 'span'},")"]
js
[{$$typeof: Symbol(react.element), type: 'button'}," (",{$$typeof: Symbol(react.element), type: 'span'},")"]
2.10 reconcileChildrenArray()
creates and deletes fibers as needed.
reconcileChildrenArray()
is a bit complex. It does extra optimization
by checking if there is re-ordering of element and tries to reuse fibers
if key
exists.
key
, I have a separate episode for this topic - How does ‘key’ work internally? List diffing in React. But in our demo, we don’t have key
, so we’ll just go to the base branch.
function reconcileChildrenArray(returnFiber: Fiber,currentFirstChild: Fiber | null,newChildren: Array<any>,lanes: Lanes,): Fiber | null {let resultingFirstChild: Fiber | null = null;let previousNewFiber: Fiber | null = null;let oldFiber = currentFirstChild;let lastPlacedIndex = 0;let newIdx = 0;let nextOldFiber = null;for (; oldFiber !== null && newIdx < newChildren.length; newIdx++) {Check current fibers against the children elements
if (oldFiber.index > newIdx) {nextOldFiber = oldFiber;oldFiber = null;} else {nextOldFiber = oldFiber.sibling;}const newFiber = updateSlot(returnFiber,oldFiber,newChildren[newIdx],lanes,);Here each fiber in the list is checked upon with new props
if (newFiber === null) {// TODO: This breaks on empty slots like null children. That's// unfortunate because it triggers the slow path all the time. We need// a better way to communicate whether this was a miss or null,// boolean, undefined, etc.if (oldFiber === null) {oldFiber = nextOldFiber;}break;}if (shouldTrackSideEffects) {if (oldFiber && newFiber.alternate === null) {// We matched the slot, but we didn't reuse the existing fiber, so we// need to delete the existing child.deleteChild(returnFiber, oldFiber);-----------}If a fiber cannot be reused, it will be marked as Deletion
which in commit phase, its DOM node is deleted
}lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);----------This tries to mark the fiber as Insertion
if (previousNewFiber === null) {// TODO: Move out of the loop. This only happens for the first run.resultingFirstChild = newFiber;} else {// TODO: Defer siblings if we're not at the right index for this slot.// I.e. if we had null values before, then we want to defer this// for each null value. However, we also don't want to call updateSlot// with the previous one.previousNewFiber.sibling = newFiber;}previousNewFiber = newFiber;oldFiber = nextOldFiber;}if (newIdx === newChildren.length) {// We've reached the end of the new children. We can delete the rest.deleteRemainingChildren(returnFiber, oldFiber);if (getIsHydrating()) {const numberOfForks = newIdx;pushTreeFork(returnFiber, numberOfForks);}return resultingFirstChild;}...return resultingFirstChild;}
function reconcileChildrenArray(returnFiber: Fiber,currentFirstChild: Fiber | null,newChildren: Array<any>,lanes: Lanes,): Fiber | null {let resultingFirstChild: Fiber | null = null;let previousNewFiber: Fiber | null = null;let oldFiber = currentFirstChild;let lastPlacedIndex = 0;let newIdx = 0;let nextOldFiber = null;for (; oldFiber !== null && newIdx < newChildren.length; newIdx++) {Check current fibers against the children elements
if (oldFiber.index > newIdx) {nextOldFiber = oldFiber;oldFiber = null;} else {nextOldFiber = oldFiber.sibling;}const newFiber = updateSlot(returnFiber,oldFiber,newChildren[newIdx],lanes,);Here each fiber in the list is checked upon with new props
if (newFiber === null) {// TODO: This breaks on empty slots like null children. That's// unfortunate because it triggers the slow path all the time. We need// a better way to communicate whether this was a miss or null,// boolean, undefined, etc.if (oldFiber === null) {oldFiber = nextOldFiber;}break;}if (shouldTrackSideEffects) {if (oldFiber && newFiber.alternate === null) {// We matched the slot, but we didn't reuse the existing fiber, so we// need to delete the existing child.deleteChild(returnFiber, oldFiber);-----------}If a fiber cannot be reused, it will be marked as Deletion
which in commit phase, its DOM node is deleted
}lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);----------This tries to mark the fiber as Insertion
if (previousNewFiber === null) {// TODO: Move out of the loop. This only happens for the first run.resultingFirstChild = newFiber;} else {// TODO: Defer siblings if we're not at the right index for this slot.// I.e. if we had null values before, then we want to defer this// for each null value. However, we also don't want to call updateSlot// with the previous one.previousNewFiber.sibling = newFiber;}previousNewFiber = newFiber;oldFiber = nextOldFiber;}if (newIdx === newChildren.length) {// We've reached the end of the new children. We can delete the rest.deleteRemainingChildren(returnFiber, oldFiber);if (getIsHydrating()) {const numberOfForks = newIdx;pushTreeFork(returnFiber, numberOfForks);}return resultingFirstChild;}...return resultingFirstChild;}
updateSlot()
basically just creates or reuse fibers with new props, with consideration of key
.
function updateSlot(returnFiber: Fiber,oldFiber: Fiber | null,newChild: any,lanes: Lanes,): Fiber | null {// Update the fiber if the keys match, otherwise return null.const key = oldFiber !== null ? oldFiber.key : null;if ((typeof newChild === 'string' && newChild !== '') ||typeof newChild === 'number') {// Text nodes don't have keys. If the previous node is implicitly keyed// we can continue to replace it without aborting even if it is not a text// node.if (key !== null) {return null;}return updateTextNode(returnFiber, oldFiber, '' + newChild, lanes);}if (typeof newChild === 'object' && newChild !== null) {switch (newChild.$$typeof) {case REACT_ELEMENT_TYPE: {if (newChild.key === key) {return updateElement(returnFiber, oldFiber, newChild, lanes);-------------} else {return null;}}...}}return null;}function updateElement(returnFiber: Fiber,current: Fiber | null,element: ReactElement,lanes: Lanes,): Fiber {const elementType = element.type;if (elementType === REACT_FRAGMENT_TYPE) {return updateFragment(returnFiber,current,element.props.children,lanes,element.key,);}if (current !== null) {if (current.elementType === elementType ||-----------------------------------if could be reused
// Keep this check inline so it only runs on the false path:(__DEV__? isCompatibleFamilyForHotReloading(current, 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) === current.type)) {// Move based on indexconst existing = useFiber(current, element.props);--------Here we see useFiber() again
existing.ref = coerceRef(returnFiber, current, element);existing.return = returnFiber;return existing;}}// Insertconst created = createFiberFromElement(element, returnFiber.mode, lanes);----------------------if we cannot reuse because types are different, we'll create the fiber from scratch
created.ref = coerceRef(returnFiber, current, element);created.return = returnFiber;return created;}
function updateSlot(returnFiber: Fiber,oldFiber: Fiber | null,newChild: any,lanes: Lanes,): Fiber | null {// Update the fiber if the keys match, otherwise return null.const key = oldFiber !== null ? oldFiber.key : null;if ((typeof newChild === 'string' && newChild !== '') ||typeof newChild === 'number') {// Text nodes don't have keys. If the previous node is implicitly keyed// we can continue to replace it without aborting even if it is not a text// node.if (key !== null) {return null;}return updateTextNode(returnFiber, oldFiber, '' + newChild, lanes);}if (typeof newChild === 'object' && newChild !== null) {switch (newChild.$$typeof) {case REACT_ELEMENT_TYPE: {if (newChild.key === key) {return updateElement(returnFiber, oldFiber, newChild, lanes);-------------} else {return null;}}...}}return null;}function updateElement(returnFiber: Fiber,current: Fiber | null,element: ReactElement,lanes: Lanes,): Fiber {const elementType = element.type;if (elementType === REACT_FRAGMENT_TYPE) {return updateFragment(returnFiber,current,element.props.children,lanes,element.key,);}if (current !== null) {if (current.elementType === elementType ||-----------------------------------if could be reused
// Keep this check inline so it only runs on the false path:(__DEV__? isCompatibleFamilyForHotReloading(current, 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) === current.type)) {// Move based on indexconst existing = useFiber(current, element.props);--------Here we see useFiber() again
existing.ref = coerceRef(returnFiber, current, element);existing.return = returnFiber;return existing;}}// Insertconst created = createFiberFromElement(element, returnFiber.mode, lanes);----------------------if we cannot reuse because types are different, we'll create the fiber from scratch
created.ref = coerceRef(returnFiber, current, element);created.return = returnFiber;return created;}
So for <div/>
,updateSlot()
successfully reused 3 children, except the 3th one because
current
is span
but we want b
, thus fiber of b
is created from scratch and
fiber of span
is deleted by deleteChild()
. The newly created b
is marked by placeChild()
.
2.11 placeChild()
and deleteChild()
marks fiber with flags
Ok, for the children of <div>
under Component
, we have these 2 functions
that marks fiber nodes.
function placeChild(newFiber: Fiber,lastPlacedIndex: number,newIndex: number,): number {newFiber.index = newIndex;if (!shouldTrackSideEffects) {// During hydration, the useId algorithm needs to know which fibers are// part of a list of children (arrays, iterators).newFiber.flags |= Forked;return lastPlacedIndex;}const current = newFiber.alternate;if (current !== null) {const oldIndex = current.index;if (oldIndex < lastPlacedIndex) {// This is a move.newFiber.flags |= Placement;return lastPlacedIndex;} else {// This item can stay in place.return oldIndex;}} else {// This is an insertion.newFiber.flags |= Placement;return lastPlacedIndex;}}
function placeChild(newFiber: Fiber,lastPlacedIndex: number,newIndex: number,): number {newFiber.index = newIndex;if (!shouldTrackSideEffects) {// During hydration, the useId algorithm needs to know which fibers are// part of a list of children (arrays, iterators).newFiber.flags |= Forked;return lastPlacedIndex;}const current = newFiber.alternate;if (current !== null) {const oldIndex = current.index;if (oldIndex < lastPlacedIndex) {// This is a move.newFiber.flags |= Placement;return lastPlacedIndex;} else {// This item can stay in place.return oldIndex;}} else {// This is an insertion.newFiber.flags |= Placement;return lastPlacedIndex;}}
function deleteChild(returnFiber: Fiber, childToDelete: Fiber): void {if (!shouldTrackSideEffects) {// Noop.return;}const deletions = returnFiber.deletions;if (deletions === null) {returnFiber.deletions = [childToDelete];returnFiber.flags |= ChildDeletion;-----------------------------------} else {deletions.push(childToDelete);}}
function deleteChild(returnFiber: Fiber, childToDelete: Fiber): void {if (!shouldTrackSideEffects) {// Noop.return;}const deletions = returnFiber.deletions;if (deletions === null) {returnFiber.deletions = [childToDelete];returnFiber.flags |= ChildDeletion;-----------------------------------} else {deletions.push(childToDelete);}}
We see that fibers that should be deleted are put temporarily in an array of its parent. This is needed because they don’t exist in the new fiber tree any longer after deletion, but they are to be processed in Commit phase, so we need to put them somewhere.
Ok we are done with <div>
.
Next React goes to button
. Again, thought it doesn’t have any schedule work,
React still works on it with updateHostComponent()
because props have changed
from ["click me-", "1"]
to ["click me-", "2"]
.
For HostText their props are string, so the first "click me -"
bails out.
And in turn, React tries to reconcile the texts with updateHostText()
.
2.12 updateHostText()
.
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;
Again it doesn’t nothing, because the updates are marked in complete phase - completeWork()
.
This is also explained in initial mount.
2.13 completeWork()
marks the update of HostComponent and creates DOM nodes if necessary
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 HostComponent: {popHostContext(workInProgress);const rootContainerInstance = getRootHostContainer();const type = workInProgress.type;if (current !== null && workInProgress.stateNode != null) {---------------------------------------------------------This is update branch we are having
updateHostComponent(current,workInProgress,type,newProps,rootContainerInstance,);if (current.ref !== workInProgress.ref) {markRef(workInProgress);}} else {This is the initial mount branch we overed in previous episode
...}bubbleProperties(workInProgress);return null;}case HostText: {const newText = newProps;if (current && workInProgress.stateNode != null) {-------------------------------------------------This is update branch we are having
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 {This is the initial mount branch we overed in previous episode
...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 HostComponent: {popHostContext(workInProgress);const rootContainerInstance = getRootHostContainer();const type = workInProgress.type;if (current !== null && workInProgress.stateNode != null) {---------------------------------------------------------This is update branch we are having
updateHostComponent(current,workInProgress,type,newProps,rootContainerInstance,);if (current.ref !== workInProgress.ref) {markRef(workInProgress);}} else {This is the initial mount branch we overed in previous episode
...}bubbleProperties(workInProgress);return null;}case HostText: {const newText = newProps;if (current && workInProgress.stateNode != null) {-------------------------------------------------This is update branch we are having
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 {This is the initial mount branch we overed in previous episode
...if (wasHydrated) {if (prepareToHydrateHostTextInstance(workInProgress)) {markUpdate(workInProgress);}} else {workInProgress.stateNode = createTextInstance(newText,rootContainerInstance,currentHostContext,workInProgress,);}}bubbleProperties(workInProgress);return null;}...}}
updateHostText = function(Notice this is a different
updateHostText()
in complete phasecurrent: Fiber,workInProgress: Fiber,oldText: string,newText: string,) {// If the text differs, mark it as an update. All the work in done in commitWork.if (oldText !== newText) {markUpdate(workInProgress);----------}};updateHostComponent = function(current: Fiber,workInProgress: Fiber,type: Type,newProps: Props,rootContainerInstance: Container,) {// If we have an alternate, that means this is an update and we need to// schedule a side-effect to do the updates.const oldProps = current.memoizedProps;if (oldProps === newProps) {// In mutation mode, this is sufficient for a bailout because// we won't touch this node even if children changed.return;}// If we get updated because one of our children updated, we don't// have newProps so we'll have to reuse them.// TODO: Split the update API as separate for the props vs. children.// Even better would be if children weren't special cased at all tho.const instance: Instance = workInProgress.stateNode;const currentHostContext = getHostContext();// TODO: Experiencing an error where oldProps is null. Suggests a host// component is hitting the resume path. Figure out why. Possibly// related to `hidden`.const updatePayload = prepareUpdate(instance,type,oldProps,newProps,rootContainerInstance,currentHostContext,);// TODO: Type this specific to this type of component.workInProgress.updateQueue = (updatePayload: any);-----------the update is put in updateQueue
This actually is also used for hooks like Effect Hooks
// If the update payload indicates that there is a change or if there// is a new ref we mark this as an update. All the work is done in commitWork.if (updatePayload) {markUpdate(workInProgress);----------}};function markUpdate(workInProgress: Fiber) {// Tag the fiber with an update effect. This turns a Placement into// a PlacementAndUpdate.workInProgress.flags |= Update;---------------Yep, another flag!
}
updateHostText = function(Notice this is a different
updateHostText()
in complete phasecurrent: Fiber,workInProgress: Fiber,oldText: string,newText: string,) {// If the text differs, mark it as an update. All the work in done in commitWork.if (oldText !== newText) {markUpdate(workInProgress);----------}};updateHostComponent = function(current: Fiber,workInProgress: Fiber,type: Type,newProps: Props,rootContainerInstance: Container,) {// If we have an alternate, that means this is an update and we need to// schedule a side-effect to do the updates.const oldProps = current.memoizedProps;if (oldProps === newProps) {// In mutation mode, this is sufficient for a bailout because// we won't touch this node even if children changed.return;}// If we get updated because one of our children updated, we don't// have newProps so we'll have to reuse them.// TODO: Split the update API as separate for the props vs. children.// Even better would be if children weren't special cased at all tho.const instance: Instance = workInProgress.stateNode;const currentHostContext = getHostContext();// TODO: Experiencing an error where oldProps is null. Suggests a host// component is hitting the resume path. Figure out why. Possibly// related to `hidden`.const updatePayload = prepareUpdate(instance,type,oldProps,newProps,rootContainerInstance,currentHostContext,);// TODO: Type this specific to this type of component.workInProgress.updateQueue = (updatePayload: any);-----------the update is put in updateQueue
This actually is also used for hooks like Effect Hooks
// If the update payload indicates that there is a change or if there// is a new ref we mark this as an update. All the work is done in commitWork.if (updatePayload) {markUpdate(workInProgress);----------}};function markUpdate(workInProgress: Fiber) {// Tag the fiber with an update effect. This turns a Placement into// a PlacementAndUpdate.workInProgress.flags |= Update;---------------Yep, another flag!
}
So we are done with the Render phase and here we got
- an Insertion on
b
- a Deletion on
span
- an update on HostText
- an update on
button
(empty under the hood).
One thing I’d like to point out is that prepareUpdate()
is run for both button
and its parent div
,
yet it generates null
for div
but []
for button
. It is some tricky edge case handling
that we won’t dive into here.
It’s time to commit these updates in Commit phase.
3. Re-render in Commit Phase
3.1 commitMutationEffectsOnFiber()
kicks off the commit of Insertion/Deletion/Update
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);----------------------------------Recursively handles children First
commitReconciliationEffects(finishedWork);----------------------------------Then Insertion
if (flags & Update) {--------------------Update at last
try {commitHookEffectListUnmount(HookInsertion | HookHasEffect,finishedWork,finishedWork.return,);commitHookEffectListMount(HookInsertion | HookHasEffect,finishedWork,);} catch (error) {captureCommitPhaseError(finishedWork, finishedWork.return, error);}...}return;}case HostComponent: {recursivelyTraverseMutationEffects(root, finishedWork, lanes);----------------------------------Recursively handles children First
commitReconciliationEffects(finishedWork);----------------------------------Then Insertion
if (supportsMutation) {// TODO: ContentReset gets cleared by the children during the commit// phase. This is a refactor hazard because it means we must read// flags the flags after `commitReconciliationEffects` has already run;// the order matters. We should refactor so that ContentReset does not// rely on mutating the flag during commit. Like by setting a flag// during the render phase instead.if (finishedWork.flags & ContentReset) {const instance: Instance = finishedWork.stateNode;try {resetTextContent(instance);} catch (error) {captureCommitPhaseError(finishedWork, finishedWork.return, error);}}if (flags & Update) {--------------------Update at last
const instance: Instance = finishedWork.stateNode;if (instance != null) {// Commit the work prepared earlier.const newProps = finishedWork.memoizedProps;// For hydration we reuse the update path but we treat the oldProps// as the newProps. The updatePayload will contain the real change in// this case.const oldProps =current !== null ? current.memoizedProps : newProps;const type = finishedWork.type;// TODO: Type the updateQueue to be specific to host components.const updatePayload: null | UpdatePayload = (finishedWork.updateQueue: any);finishedWork.updateQueue = null;if (updatePayload !== null) {try {commitUpdate(------------For HostComponent, we are just updating the props
instance,updatePayload,type,oldProps,newProps,finishedWork,);} catch (error) {captureCommitPhaseError(finishedWork,finishedWork.return,error,);}}}}}return;}case HostText: {recursivelyTraverseMutationEffects(root, finishedWork, lanes);commitReconciliationEffects(finishedWork);if (flags & Update) {if (supportsMutation) {if (finishedWork.stateNode === null) {throw new Error('This should have a text node initialized. This error is likely ' +'caused by a bug in React. Please file an issue.',);}const textInstance: TextInstance = finishedWork.stateNode;const newText: string = finishedWork.memoizedProps;// For hydration we reuse the update path but we treat the oldProps// as the newProps. The updatePayload will contain the real change in// this case.const oldText: string =current !== null ? current.memoizedProps : newText;try {commitTextUpdate(textInstance, oldText, newText);----------------For HostText, we are just updating the textContent
} catch (error) {captureCommitPhaseError(finishedWork, finishedWork.return, error);}}}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);----------------------------------Recursively handles children First
commitReconciliationEffects(finishedWork);----------------------------------Then Insertion
if (flags & Update) {--------------------Update at last
try {commitHookEffectListUnmount(HookInsertion | HookHasEffect,finishedWork,finishedWork.return,);commitHookEffectListMount(HookInsertion | HookHasEffect,finishedWork,);} catch (error) {captureCommitPhaseError(finishedWork, finishedWork.return, error);}...}return;}case HostComponent: {recursivelyTraverseMutationEffects(root, finishedWork, lanes);----------------------------------Recursively handles children First
commitReconciliationEffects(finishedWork);----------------------------------Then Insertion
if (supportsMutation) {// TODO: ContentReset gets cleared by the children during the commit// phase. This is a refactor hazard because it means we must read// flags the flags after `commitReconciliationEffects` has already run;// the order matters. We should refactor so that ContentReset does not// rely on mutating the flag during commit. Like by setting a flag// during the render phase instead.if (finishedWork.flags & ContentReset) {const instance: Instance = finishedWork.stateNode;try {resetTextContent(instance);} catch (error) {captureCommitPhaseError(finishedWork, finishedWork.return, error);}}if (flags & Update) {--------------------Update at last
const instance: Instance = finishedWork.stateNode;if (instance != null) {// Commit the work prepared earlier.const newProps = finishedWork.memoizedProps;// For hydration we reuse the update path but we treat the oldProps// as the newProps. The updatePayload will contain the real change in// this case.const oldProps =current !== null ? current.memoizedProps : newProps;const type = finishedWork.type;// TODO: Type the updateQueue to be specific to host components.const updatePayload: null | UpdatePayload = (finishedWork.updateQueue: any);finishedWork.updateQueue = null;if (updatePayload !== null) {try {commitUpdate(------------For HostComponent, we are just updating the props
instance,updatePayload,type,oldProps,newProps,finishedWork,);} catch (error) {captureCommitPhaseError(finishedWork,finishedWork.return,error,);}}}}}return;}case HostText: {recursivelyTraverseMutationEffects(root, finishedWork, lanes);commitReconciliationEffects(finishedWork);if (flags & Update) {if (supportsMutation) {if (finishedWork.stateNode === null) {throw new Error('This should have a text node initialized. This error is likely ' +'caused by a bug in React. Please file an issue.',);}const textInstance: TextInstance = finishedWork.stateNode;const newText: string = finishedWork.memoizedProps;// For hydration we reuse the update path but we treat the oldProps// as the newProps. The updatePayload will contain the real change in// this case.const oldText: string =current !== null ? current.memoizedProps : newText;try {commitTextUpdate(textInstance, oldText, newText);----------------For HostText, we are just updating the textContent
} catch (error) {captureCommitPhaseError(finishedWork, finishedWork.return, error);}}}return;}}}
We can see that it is a recursive process and let’s take a closer look at each type of mutation.
3.2. Deletion are processed first, before processing children and self.
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);}}}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);}}}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);}
Deletions are processed first, even before processing children.
function commitDeletionEffects(root: FiberRoot,returnFiber: Fiber,deletedFiber: Fiber,) {if (supportsMutation) {// We only have the top Fiber that was deleted but we need to recurse down its// children to find all the terminal nodes.// Recursively delete all host nodes from the parent, detach refs, clean// up mounted layout effects, and call componentWillUnmount.// We only need to remove the topmost host child in each branch. But then we// still need to keep traversing to unmount effects, refs, and cWU. TODO: We// could split this into two separate traversals functions, where the second// one doesn't include any removeChild logic. This is maybe the same// function as "disappearLayoutEffects" (or whatever that turns into after// the layout phase is refactored to use recursion).// Before starting, find the nearest host parent on the stack so we know// which instance/container to remove the children from.// TODO: Instead of searching up the fiber return path on every deletion, we// can track the nearest host component on the JS stack as we traverse the// tree during the commit phase. This would make insertions faster, too.let parent = returnFiber;findParent: while (parent !== null) {--------------------------------Since parent node doesn't necessary means it has backing DOM
Here it searches for the closest Fiber Node that has backingDOM
switch (parent.tag) {case HostComponent: {hostParent = parent.stateNode;hostParentIsContainer = false;break findParent;}case HostRoot: {hostParent = parent.stateNode.containerInfo;hostParentIsContainer = true;break findParent;}case HostPortal: {hostParent = parent.stateNode.containerInfo;hostParentIsContainer = true;break findParent;}}parent = parent.return;}if (hostParent === null) {throw new Error('Expected to find a host parent. This error is likely caused by ' +'a bug in React. Please file an issue.',);}commitDeletionEffectsOnFiber(root, returnFiber, deletedFiber);----------------------------hostParent = null;hostParentIsContainer = false;} else {// Detach refs and call componentWillUnmount() on the whole subtree.commitDeletionEffectsOnFiber(root, returnFiber, deletedFiber);----------------------------}detachFiberMutation(deletedFiber);}
function commitDeletionEffects(root: FiberRoot,returnFiber: Fiber,deletedFiber: Fiber,) {if (supportsMutation) {// We only have the top Fiber that was deleted but we need to recurse down its// children to find all the terminal nodes.// Recursively delete all host nodes from the parent, detach refs, clean// up mounted layout effects, and call componentWillUnmount.// We only need to remove the topmost host child in each branch. But then we// still need to keep traversing to unmount effects, refs, and cWU. TODO: We// could split this into two separate traversals functions, where the second// one doesn't include any removeChild logic. This is maybe the same// function as "disappearLayoutEffects" (or whatever that turns into after// the layout phase is refactored to use recursion).// Before starting, find the nearest host parent on the stack so we know// which instance/container to remove the children from.// TODO: Instead of searching up the fiber return path on every deletion, we// can track the nearest host component on the JS stack as we traverse the// tree during the commit phase. This would make insertions faster, too.let parent = returnFiber;findParent: while (parent !== null) {--------------------------------Since parent node doesn't necessary means it has backing DOM
Here it searches for the closest Fiber Node that has backingDOM
switch (parent.tag) {case HostComponent: {hostParent = parent.stateNode;hostParentIsContainer = false;break findParent;}case HostRoot: {hostParent = parent.stateNode.containerInfo;hostParentIsContainer = true;break findParent;}case HostPortal: {hostParent = parent.stateNode.containerInfo;hostParentIsContainer = true;break findParent;}}parent = parent.return;}if (hostParent === null) {throw new Error('Expected to find a host parent. This error is likely caused by ' +'a bug in React. Please file an issue.',);}commitDeletionEffectsOnFiber(root, returnFiber, deletedFiber);----------------------------hostParent = null;hostParentIsContainer = false;} else {// Detach refs and call componentWillUnmount() on the whole subtree.commitDeletionEffectsOnFiber(root, returnFiber, deletedFiber);----------------------------}detachFiberMutation(deletedFiber);}
function commitDeletionEffectsOnFiber(finishedRoot: FiberRoot,nearestMountedAncestor: Fiber,deletedFiber: Fiber,) {onCommitUnmount(deletedFiber);// The cases in this outer switch modify the stack before they traverse// into their subtree. There are simpler cases in the inner switch// that don't modify the stack.switch (deletedFiber.tag) {case HostComponent: {if (!offscreenSubtreeWasHidden) {safelyDetachRef(deletedFiber, nearestMountedAncestor);}// Intentional fallthrough to next branch}// eslint-disable-next-line-no-fallthroughcase HostText: {// We only need to remove the nearest host child. Set the host parent// to `null` on the stack to indicate that nested children don't// need to be removed.if (supportsMutation) {const prevHostParent = hostParent;const prevHostParentIsContainer = hostParentIsContainer;hostParent = null;recursivelyTraverseDeletionEffects(finishedRoot,nearestMountedAncestor,deletedFiber,);hostParent = prevHostParent;hostParentIsContainer = prevHostParentIsContainer;if (hostParent !== null) {// Now that all the child effects have unmounted, we can remove the// node from the tree.if (hostParentIsContainer) {removeChildFromContainer(-----------------------((hostParent: any): Container),---------This hostParent is retried from previous while loop
(deletedFiber.stateNode: Instance | TextInstance),);} else {removeChild(------------((hostParent: any): Instance),(deletedFiber.stateNode: Instance | TextInstance),);}}} else {recursivelyTraverseDeletionEffects(finishedRoot,nearestMountedAncestor,deletedFiber,);}return;}...default: {recursivelyTraverseDeletionEffects(finishedRoot,nearestMountedAncestor,deletedFiber,);return;}}}
function commitDeletionEffectsOnFiber(finishedRoot: FiberRoot,nearestMountedAncestor: Fiber,deletedFiber: Fiber,) {onCommitUnmount(deletedFiber);// The cases in this outer switch modify the stack before they traverse// into their subtree. There are simpler cases in the inner switch// that don't modify the stack.switch (deletedFiber.tag) {case HostComponent: {if (!offscreenSubtreeWasHidden) {safelyDetachRef(deletedFiber, nearestMountedAncestor);}// Intentional fallthrough to next branch}// eslint-disable-next-line-no-fallthroughcase HostText: {// We only need to remove the nearest host child. Set the host parent// to `null` on the stack to indicate that nested children don't// need to be removed.if (supportsMutation) {const prevHostParent = hostParent;const prevHostParentIsContainer = hostParentIsContainer;hostParent = null;recursivelyTraverseDeletionEffects(finishedRoot,nearestMountedAncestor,deletedFiber,);hostParent = prevHostParent;hostParentIsContainer = prevHostParentIsContainer;if (hostParent !== null) {// Now that all the child effects have unmounted, we can remove the// node from the tree.if (hostParentIsContainer) {removeChildFromContainer(-----------------------((hostParent: any): Container),---------This hostParent is retried from previous while loop
(deletedFiber.stateNode: Instance | TextInstance),);} else {removeChild(------------((hostParent: any): Instance),(deletedFiber.stateNode: Instance | TextInstance),);}}} else {recursivelyTraverseDeletionEffects(finishedRoot,nearestMountedAncestor,deletedFiber,);}return;}...default: {recursivelyTraverseDeletionEffects(finishedRoot,nearestMountedAncestor,deletedFiber,);return;}}}
3.3 Insertions are processed next
This is so that newly created nodes could be set up in a tree structure.
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) {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;}if (flags & Hydrating) {finishedWork.flags &= ~Hydrating;}}function commitPlacement(finishedWork: Fiber): void {if (!supportsMutation) {return;}// Recursively insert all host nodes into the parent.const parentFiber = getHostParentFiber(finishedWork);// Note: these two variables *must* always be updated together.switch (parentFiber.tag) {case HostComponent: {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);This is important. Node.insertBefore() needs the sibling node
If we cannot find it, then we just append it to the end
// 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:case HostPortal: {const parent: Container = parentFiber.stateNode.containerInfo;const before = getHostSibling(finishedWork);insertOrAppendPlacementNodeIntoContainer(finishedWork, before, parent);break;}// eslint-disable-next-line-no-fallthroughdefault:throw new Error('Invalid host parent fiber. This error is likely caused by a bug ' +'in React. Please file an issue.',);}}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);}} else if (tag === HostPortal) {// 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.} 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;}}}}function insertOrAppendPlacementNode(node: Fiber,before: ?Instance,parent: Instance,): void {const {tag} = node;const isHost = tag === HostComponent || tag === HostText;if (isHost) {const stateNode = node.stateNode;if (before) {insertBefore(parent, stateNode, before);} else {appendChild(parent, stateNode);}} else if (tag === HostPortal) {// 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.} else {const child = node.child;if (child !== null) {insertOrAppendPlacementNode(child, before, parent);let sibling = child.sibling;while (sibling !== null) {insertOrAppendPlacementNode(sibling, before, parent);sibling = sibling.sibling;}}}}
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) {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;}if (flags & Hydrating) {finishedWork.flags &= ~Hydrating;}}function commitPlacement(finishedWork: Fiber): void {if (!supportsMutation) {return;}// Recursively insert all host nodes into the parent.const parentFiber = getHostParentFiber(finishedWork);// Note: these two variables *must* always be updated together.switch (parentFiber.tag) {case HostComponent: {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);This is important. Node.insertBefore() needs the sibling node
If we cannot find it, then we just append it to the end
// 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:case HostPortal: {const parent: Container = parentFiber.stateNode.containerInfo;const before = getHostSibling(finishedWork);insertOrAppendPlacementNodeIntoContainer(finishedWork, before, parent);break;}// eslint-disable-next-line-no-fallthroughdefault:throw new Error('Invalid host parent fiber. This error is likely caused by a bug ' +'in React. Please file an issue.',);}}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);}} else if (tag === HostPortal) {// 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.} 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;}}}}function insertOrAppendPlacementNode(node: Fiber,before: ?Instance,parent: Instance,): void {const {tag} = node;const isHost = tag === HostComponent || tag === HostText;if (isHost) {const stateNode = node.stateNode;if (before) {insertBefore(parent, stateNode, before);} else {appendChild(parent, stateNode);}} else if (tag === HostPortal) {// 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.} else {const child = node.child;if (child !== null) {insertOrAppendPlacementNode(child, before, parent);let sibling = child.sibling;while (sibling !== null) {insertOrAppendPlacementNode(sibling, before, parent);sibling = sibling.sibling;}}}}
3.4 Updates are handled at last
Update branch is inside commitMutationEffectsOnFiber()
.
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);commitReconciliationEffects(finishedWork);if (flags & Update) {-------------------For FunctionComponent, this means hooks need to be run
try {commitHookEffectListUnmount(HookInsertion | HookHasEffect,finishedWork,finishedWork.return,);commitHookEffectListMount(HookInsertion | HookHasEffect,finishedWork,);} catch (error) {captureCommitPhaseError(finishedWork, finishedWork.return, error);}// Layout effects are destroyed during the mutation phase so that all// destroy functions for all fibers are called before any create functions.// This prevents sibling component effects from interfering with each other,// e.g. a destroy function in one component should never override a ref set// by a create function in another component during the same commit.if (enableProfilerTimer &&enableProfilerCommitHooks &&finishedWork.mode & ProfileMode) {try {startLayoutEffectTimer();commitHookEffectListUnmount(HookLayout | HookHasEffect,finishedWork,finishedWork.return,);} catch (error) {captureCommitPhaseError(finishedWork, finishedWork.return, error);}recordLayoutEffectDuration(finishedWork);} else {try {commitHookEffectListUnmount(HookLayout | HookHasEffect,finishedWork,finishedWork.return,);} catch (error) {captureCommitPhaseError(finishedWork, finishedWork.return, error);}}}return;}case HostComponent: {recursivelyTraverseMutationEffects(root, finishedWork, lanes);commitReconciliationEffects(finishedWork);if (flags & Ref) {if (current !== null) {safelyDetachRef(current, current.return);}}if (supportsMutation) {// TODO: ContentReset gets cleared by the children during the commit// phase. This is a refactor hazard because it means we must read// flags the flags after `commitReconciliationEffects` has already run;// the order matters. We should refactor so that ContentReset does not// rely on mutating the flag during commit. Like by setting a flag// during the render phase instead.if (finishedWork.flags & ContentReset) {const instance: Instance = finishedWork.stateNode;try {resetTextContent(instance);} catch (error) {captureCommitPhaseError(finishedWork, finishedWork.return, error);}}if (flags & Update) {-------------------For HostComponent, this means element attributes are needed to be updated
const instance: Instance = finishedWork.stateNode;if (instance != null) {// Commit the work prepared earlier.const newProps = finishedWork.memoizedProps;// For hydration we reuse the update path but we treat the oldProps// as the newProps. The updatePayload will contain the real change in// this case.const oldProps =current !== null ? current.memoizedProps : newProps;const type = finishedWork.type;// TODO: Type the updateQueue to be specific to host components.const updatePayload: null | UpdatePayload = (finishedWork.updateQueue: any);finishedWork.updateQueue = null;if (updatePayload !== null) {try {commitUpdate(------------instance,updatePayload,type,oldProps,newProps,finishedWork,);} catch (error) {captureCommitPhaseError(finishedWork,finishedWork.return,error,);}}}}}return;}case HostText: {recursivelyTraverseMutationEffects(root, finishedWork, lanes);commitReconciliationEffects(finishedWork);if (flags & Update) {------------------if (supportsMutation) {if (finishedWork.stateNode === null) {throw new Error('This should have a text node initialized. This error is likely ' +'caused by a bug in React. Please file an issue.',);}const textInstance: TextInstance = finishedWork.stateNode;const newText: string = finishedWork.memoizedProps;// For hydration we reuse the update path but we treat the oldProps// as the newProps. The updatePayload will contain the real change in// this case.const oldText: string =current !== null ? current.memoizedProps : newText;try {commitTextUpdate(textInstance, oldText, newText);----------------} catch (error) {captureCommitPhaseError(finishedWork, finishedWork.return, error);}}}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);commitReconciliationEffects(finishedWork);if (flags & Update) {-------------------For FunctionComponent, this means hooks need to be run
try {commitHookEffectListUnmount(HookInsertion | HookHasEffect,finishedWork,finishedWork.return,);commitHookEffectListMount(HookInsertion | HookHasEffect,finishedWork,);} catch (error) {captureCommitPhaseError(finishedWork, finishedWork.return, error);}// Layout effects are destroyed during the mutation phase so that all// destroy functions for all fibers are called before any create functions.// This prevents sibling component effects from interfering with each other,// e.g. a destroy function in one component should never override a ref set// by a create function in another component during the same commit.if (enableProfilerTimer &&enableProfilerCommitHooks &&finishedWork.mode & ProfileMode) {try {startLayoutEffectTimer();commitHookEffectListUnmount(HookLayout | HookHasEffect,finishedWork,finishedWork.return,);} catch (error) {captureCommitPhaseError(finishedWork, finishedWork.return, error);}recordLayoutEffectDuration(finishedWork);} else {try {commitHookEffectListUnmount(HookLayout | HookHasEffect,finishedWork,finishedWork.return,);} catch (error) {captureCommitPhaseError(finishedWork, finishedWork.return, error);}}}return;}case HostComponent: {recursivelyTraverseMutationEffects(root, finishedWork, lanes);commitReconciliationEffects(finishedWork);if (flags & Ref) {if (current !== null) {safelyDetachRef(current, current.return);}}if (supportsMutation) {// TODO: ContentReset gets cleared by the children during the commit// phase. This is a refactor hazard because it means we must read// flags the flags after `commitReconciliationEffects` has already run;// the order matters. We should refactor so that ContentReset does not// rely on mutating the flag during commit. Like by setting a flag// during the render phase instead.if (finishedWork.flags & ContentReset) {const instance: Instance = finishedWork.stateNode;try {resetTextContent(instance);} catch (error) {captureCommitPhaseError(finishedWork, finishedWork.return, error);}}if (flags & Update) {-------------------For HostComponent, this means element attributes are needed to be updated
const instance: Instance = finishedWork.stateNode;if (instance != null) {// Commit the work prepared earlier.const newProps = finishedWork.memoizedProps;// For hydration we reuse the update path but we treat the oldProps// as the newProps. The updatePayload will contain the real change in// this case.const oldProps =current !== null ? current.memoizedProps : newProps;const type = finishedWork.type;// TODO: Type the updateQueue to be specific to host components.const updatePayload: null | UpdatePayload = (finishedWork.updateQueue: any);finishedWork.updateQueue = null;if (updatePayload !== null) {try {commitUpdate(------------instance,updatePayload,type,oldProps,newProps,finishedWork,);} catch (error) {captureCommitPhaseError(finishedWork,finishedWork.return,error,);}}}}}return;}case HostText: {recursivelyTraverseMutationEffects(root, finishedWork, lanes);commitReconciliationEffects(finishedWork);if (flags & Update) {------------------if (supportsMutation) {if (finishedWork.stateNode === null) {throw new Error('This should have a text node initialized. This error is likely ' +'caused by a bug in React. Please file an issue.',);}const textInstance: TextInstance = finishedWork.stateNode;const newText: string = finishedWork.memoizedProps;// For hydration we reuse the update path but we treat the oldProps// as the newProps. The updatePayload will contain the real change in// this case.const oldText: string =current !== null ? current.memoizedProps : newText;try {commitTextUpdate(textInstance, oldText, newText);----------------} catch (error) {captureCommitPhaseError(finishedWork, finishedWork.return, error);}}}return;}...default: {recursivelyTraverseMutationEffects(root, finishedWork, lanes);commitReconciliationEffects(finishedWork);return;}}}
export function commitUpdate(domElement: Instance,updatePayload: Array<mixed>,type: string,oldProps: Props,newProps: Props,internalInstanceHandle: Object,): void {// Apply the diff to the DOM node.updateProperties(domElement, updatePayload, type, oldProps, newProps);----------------// Update the props handle so that we know which props are the ones with// with current event handlers.updateFiberProps(domElement, newProps);}export function commitTextUpdate(textInstance: TextInstance,oldText: string,newText: string,): void {textInstance.nodeValue = newText;}
export function commitUpdate(domElement: Instance,updatePayload: Array<mixed>,type: string,oldProps: Props,newProps: Props,internalInstanceHandle: Object,): void {// Apply the diff to the DOM node.updateProperties(domElement, updatePayload, type, oldProps, newProps);----------------// Update the props handle so that we know which props are the ones with// with current event handlers.updateFiberProps(domElement, newProps);}export function commitTextUpdate(textInstance: TextInstance,oldText: string,newText: string,): void {textInstance.nodeValue = newText;}
For our demo, because of the tree structure, the mutations are handled in below order.
- Deletion on
span
- Update on HostText
- Update on
button
(empty under the hood) - Insertion of
b
4. Summary
Phew that is a lot. I’d roughly summarize the re-render process as below.
- After state changes, paths to target Fiber Node are marked with
lanes
andchildLanes
, to indicate if they or their subtree need to be re-rendered. - React re-render the whole Fiber Tree, with optimization of bailout to avoid re-rendering unnecessarily.
- Once a component re-renders, it generates new React elements, its children all get
new props even though they are equal, so React re-renders the whole Fiber Tree by default.
Here is why you might need
useMemo()
. - By “re-render”, React creates a new Fiber Tree out of current one, also marks the Fiber
Node with flags of
Placement
ChildDeletion
andUpdate
if needed. - Once the new Fiber Tree is done, React processes Fiber Nodes with above flags and apply the changes to Host DOM in Commit phase.
- Then the new Fiber Tree is pointed to as the current Fiber Tree. The nodes on previous Fiber Tree could be reused for next render.
I’ve put the steps into slides below, hope it helps.
Want to know more about how React works internally?
Check out my series - React Internals Deep Dive!