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.

import {useState} from 'react'
  function Link() {
    return <a href="https://jser.dev">jser.dev</a>;
  }

  function Component() {
    const [count, setCount] = useState(0);
    return (
      <div>
      <button onClick={() => setCount((count) => count + 1)}>
        click me - {count} 
      </button> ({count % 2 === 0 ? <span>even</span> : <b>odd</b>})
      </div>
    );
  }
  export default function App() {
    return (
      <div>
        <Link />
        <br />
        <Component />
      </div>
    );
  }

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:

  1. lanes => for the pending work of itself
  2. childLanes => for the pending work of its subtree

When the button is clicked, setState() is called:

  1. the path from root to the target fiber will be marked with lanes and childLanes to indicate places that need to be checked in next render.
  2. an update is scheduled by scheduleUpdateOnFiber(), in which eventually ensureRootIsScheduled() is called and performConcurrentWorkOnRoot() 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.

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 HostRoot

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

The current of root is the FiberNode of HostRoot

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

So 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 equal

which 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 fibers

current,
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 equal

which 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 fibers

current,
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 dependencies
workInProgress.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 dependencies
workInProgress.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

  1. if a fiber doesn’t have props/context changes and it doesn’t have pending work (empty lanes)
    1. if its children don’t have pending work(empty childLanes), bailout happens and React doesn’t go deeper in the tree.
    2. otherwise React goes straight to its children, without re-rendering this fiber
  2. 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 reconciliation
workInProgress.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 reconciliation
workInProgress.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 constructors
return 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 constructors
return 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.

  1. React works on HostRoot(lanes: 0, childLanes: 1). HostRoot doesn’t have props and memoizedProps and pendingProps are both null, so React goes straight to its child, which is the cloned App.
  2. React works on <App/>(lanes: 0, childLanes: 1). App component is not re-rendered so memoizedProps and pendingProps are the same, so React goes straight to its child, which is the cloned div.
  3. 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/>.
  4. 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/>.
  5. 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 types
if (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 types
if (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 equal

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;
}
const oldProps = current.memoizedProps;
const newProps = workInProgress.pendingProps;
if (
oldProps !== newProps ||
---------------------

Here it is using === not shallow equal

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;
}

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.

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 index
const existing = useFiber(current, element.props);
--------

Here we see useFiber() again

existing.ref = coerceRef(returnFiber, current, element);
existing.return = returnFiber;
return existing;
}
}
// Insert
const 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 index
const existing = useFiber(current, element.props);
--------

Here we see useFiber() again

existing.ref = coerceRef(returnFiber, current, element);
existing.return = returnFiber;
return existing;
}
}
// Insert
const 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 4th one because current is span but we want b, thus fiber of span is created from scratch and fiber of b is deleted by deleteChild(). The newly created span 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 phase

current: 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 phase

current: 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

  1. an Insertion on b
  2. a Deletion on span
  3. an update on HostText
  4. 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-fallthrough
case 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-fallthrough
case 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 insertions
resetTextContent(parent);
// Clear ContentReset from the effect tag
parentFiber.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-fallthrough
default:
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 insertions
resetTextContent(parent);
// Clear ContentReset from the effect tag
parentFiber.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-fallthrough
default:
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.

  1. Deletion on span
  2. Update on HostText
  3. Update on button (empty under the hood)
  4. Insertion of b

4. Summary

Phew that is a lot. I’d roughly summarize the re-render process as below.

  1. After state changes, paths to target Fiber Node are marked with lanes and childLanes, to indicate if they or their subtree need to be re-rendered.
  2. React re-render the whole Fiber Tree, with optimization of bailout to avoid re-rendering unnecessarily.
  3. 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().
  4. By “re-render”, React creates a new Fiber Tree out of current one, also marks the Fiber Node with flags of Placement ChildDeletion and Update if needed.
  5. Once the new Fiber Tree is done, React processes Fiber Nodes with above flags and apply the changes to Host DOM in Commit phase.
  6. 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.

How React re-renders internally

(1/31)

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

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

❮ Prev: How does React do the initial mount internally?

Next: How lazy() works internally in React?