In previous articles, we learned that the two most important processes during the render phase are beginWork and completeWork. This article will explore the different behaviors of beginWork during the initialization and update phases. I will first identify their commonalities and then analyze them step-by-step through the source code, ultimately clarifying this process.
Both the initialization and update processes fundamentally rely on three basic elements:
current: The current Fiber node
workInProgress: The Fiber node being constructed
renderLanes: The priority level of the current update task
beginWork operates on a specific Fiber node, processing only the current node at each step. After processing, it continuously looks for its child nodes until it encounters a null reference, signaling the end of the current beginWork cycle. By the end of this section, you will gain a deeper understanding of the following questions:
What are the similarities and differences of
beginWorkin mount and update scenarios?How does React reuse Fiber nodes?
Why is there an IndeterminateComponent intermediate state?
What is the state calculation mechanism when there are multiple priorities?
And more...
II. Initialization Scenario
The process of beginWork itself is not difficult; rather, it is the extensive content and numerous branches that make it complex. Therefore, it suffices to grasp the classic scenarios of beginWork. The source code for beginWork is as follows:
function beginWork(current, workInProgress, renderLanes) {
if (current !== null) { // Indicates the update phase
// The mount phase can be ignored here since it won't reach this block
}
// Clear lanes after reconciling
workInProgress.lanes = NoLanes;
// Determine based on tag
switch (workInProgress.tag) {
case IndeterminateComponent: { // Intermediate state for functional components
return mountIndeterminateComponent(
current,
workInProgress,
workInProgress.type,
renderLanes
);
}
case LazyComponent: { // Asynchronous component
var elementType = workInProgress.elementType;
return mountLazyComponent(
current,
workInProgress,
elementType,
renderLanes
);
}
case HostRoot:
return updateHostRoot(current, workInProgress, renderLanes);
case HostComponent:
return updateHostComponent(current, workInProgress, renderLanes);
case HostText:
return updateHostText(current, workInProgress);
case SuspenseComponent:
return updateSuspenseComponent(current, workInProgress, renderLanes);
case Fragment:
return updateFragment(current, workInProgress, renderLanes);
case Mode:
return updateMode(current, workInProgress, renderLanes);
case Profiler:
return updateProfiler(current, workInProgress, renderLanes);
case ContextProvider:
return updateContextProvider(current, workInProgress, renderLanes);
case ContextConsumer:
return updateContextConsumer(current, workInProgress, renderLanes);
...
}
...
}While the logic may seem overwhelming, the basic structure is as follows:
function beginWork(current, workInProgress, renderLanes) {
if (isUpdatePhase) {
if (canReuseFiber) {
return newFiberBasedOnOriginalFiber;
}
cannotReuse; // Mark the Fiber
}
// For the mount phase, go directly here since everything needs to be rebuilt
const tag = workInProgress.tag;
// Branch based on the type of Fiber node
if (fiberNodeIsRootFiberType) {
return xxx;
}
if (fiberNodeIsFunctionalComponent) {
return xxx;
}
if (fiberNodeIsNativeDOMNode) {
return xxx;
}
...
}During the mount phase, we primarily need to understand the initialization of a few key types of Fiber nodes:
HostRoot: This node will always be encountered during both initialization and updates.
mountIndeterminateComponent: This is encountered when initializing a custom component.
HostComponent: This is the basis for creating actual DOM nodes and is fundamental for UI construction.
We will delve deeper into other nodes in a later theoretical discussion.
HostRoot
Every render cycle starts from the RootFiber node, which corresponds to HostRoot because the tag attribute of RootFiber is 3. Based on what we previously learned, it falls under the HostRoot type of Fiber, as shown below:

export const FunctionComponent = 0; export const ClassComponent = 1; export const IndeterminateComponent = 2; export const HostRoot = 3; export const HostPortal = 4; export const HostComponent = 5; export const HostText = 6; export const Fragment = 7; ...
Therefore, during the mount phase, we directly call updateHostRoot(current, workInProgress, renderLanes). This node is unique in that it is the only one with a current in the mount phase. At this moment, the structure in memory consists of one current and one workInProgress node. Let's see what happens with HostRoot:
function updateHostRoot(current, workInProgress, renderLanes) {
var nextProps = workInProgress.pendingProps;
var prevState = workInProgress.memoizedState;
var prevChildren = prevState.element;
// Separate the updateQueue
cloneUpdateQueue(current, workInProgress);
// Calculate state, which basically transfers the element from updateQueue to memoizedState
processUpdateQueue(workInProgress, nextProps, null, renderLanes);
// Now memoizedState contains the <App/> ReactElement node
var nextState = workInProgress.memoizedState;
var nextChildren = nextState.element; // <App/>
...
// Create the first component node based on the current <App/>
reconcileChildren(current, workInProgress, nextChildren, renderLanes);
return workInProgress.child;
}The primary job of updateHostRoot is to produce child Fiber nodes for workInProgress. This involves some steps, such as:
The first update task created during the initialization process is stored in the updateQueue of the RootFiber node. Now, it needs to be extracted and placed into memoizedState, then the <App> ReactElement node is extracted for the next step to generate its child nodes.

Next, reconcileChildren is called to generate its child nodes:
function reconcileChildren(current, workInProgress, nextChildren, renderLanes) {
if (current === null) {
// Initialization
workInProgress.child = mountChildFibers(
workInProgress,
null,
nextChildren,
renderLanes
);
} else {
// Update
workInProgress.child = reconcileChildFibers(
workInProgress,
current.child,
nextChildren,
renderLanes
);
}
}For the HostRoot node, it is the only one that has a current during initialization. All other nodes will follow the first branch during initialization. In reconcileChildFibers, there are two scenarios: single nodes and multiple nodes. If nextChildren is an object, a single Fiber will be generated. If it is an array, a Fiber linked list will be created, linking according to the rules of the Fiber tree. The return is the first node; generally, we will only pass a single node at the root, so we follow the logic to generate a single Fiber node in reconcileSingleElement:
function reconcileSingleElement(
returnFiber,
currentFirstChild, // During initialization, this is null since there is only one RootFiber node and no child nodes
element,
lanes
) {
var key = element.key;
var _created = createFiberFromElement(
element,
returnFiber.mode,
lanes
);
_created.ref = coerceRef(returnFiber, currentFirstChild, element);
_created.return = returnFiber;
return _created;
}When creating a Fiber node, if the encountered root component is a functional component, it will be marked with IndeterminateComponent. If it is a class component, it will be marked as ClassComponent. So how do we distinguish between a class component and a functional component?
function shouldConstruct$1(Component) {
var prototype = Component.prototype;
return !!(prototype && prototype.isReactComponent);
}This is the basis for the judgment because class components are classes that extend React.Component, while functional components are pure functions, allowing us to distinguish between them.
The above outlines the reconciliation process for HostRoot, resulting in the production of its child node. If the child node is a Fiber representing a functional component class, it will be returned and assigned to the new workInProgress, entering the next round of beginWork.
IndeterminateComponent
The IndeterminateComponent class in Fiber essentially represents a functional Fiber node that React initially recognizes as a function component. You may wonder why there is an intermediate state like this. Let’s delve into why this exists.
The purpose of reconciling IndeterminateComponent during the initialization phase is to generate its child nodes. To do this, the function component needs to be invoked, as this is the only way to retrieve the latest ReactElement, and any hooks within the function component will also be executed in the process.
Code Breakdown
function mountIndeterminateComponent(
_current, // For mounts, this is null
workInProgress,
Component,
renderLanes
) {
var props = workInProgress.pendingProps;
var value;
// Logic for class components that don’t extend React.Component
setIsRendering(true);
// Invoke the function component
value = renderWithHooks(
null,
workInProgress,
Component,
props,
context,
renderLanes
);
setIsRendering(false);
workInProgress.flags |= PerformedWork; // Tagging
// If it returns an instance of a class component
if (
typeof value === "object" &&
value !== null &&
typeof value.render === "function" &&
value.$$typeof === undefined
) {
// Branch 1
workInProgress.tag = ClassComponent; // Mark as a class component
workInProgress.memoizedState = value.state !== undefined ? value.state : null;
initializeUpdateQueue(workInProgress);
adoptClassInstance(workInProgress, value);
mountClassInstance(workInProgress, Component, props, renderLanes);
return finishClassComponent(
null,
workInProgress,
Component,
true,
hasContext,
renderLanes
);
} else {
// Branch 2 - For regular function components
workInProgress.tag = FunctionComponent;
return workInProgress.child;
}
}Why Does mountIndeterminateComponent Exist?
This intermediate state exists for performance optimization in certain scenarios. Since function components are user-provided, users could write something like the following:
// Class
class ClassComponent extends React.Component {
constructor(props) {
super(props);
this.state = { num: 1 };
}
render() {
const { num } = this.state;
const onClick = () => {
this.setState({ num: num + 1 });
};
return (
<div>
<button onClick={onClick}>{num}</button>
</div>
);
}
}
// Function
const FunctionComponent = () => {
const [count, setCount] = React.useState(1);
const onClick = () => {
setCount(count + 1);
};
const instance = new ClassComponent();
return instance;
};
ReactDOM.createRoot(<FunctionComponent />).render(container);In this scenario, mountIndeterminateComponent will enter branch 1, where beginWork marks this function component as a class component. The rationale is that their state can be treated as the node’s state. This approach optimizes performance by avoiding an extra cycle of beginWork that would otherwise be required if it were treated as two separate nodes.

In the next cycle, this function component will be handled as a class component. Function components that return regular ReactElement objects proceed through branch 2 of mountIndeterminateComponent, generating their child nodes. However, before that, an important step occurs: calling the render function renderWithHooks. This function invocation triggers the user-written components in React, along with the corresponding hooks.
Code Breakdown
function renderWithHooks(
current, // For initialization, this is null
workInProgress,
Component, // The component
props,
secondArg,
nextRenderLanes
) {
renderLanes = nextRenderLanes;
currentlyRenderingFiber$1 = workInProgress;
workInProgress.memoizedState = null;
workInProgress.updateQueue = null;
workInProgress.lanes = NoLanes;
if (current && current.memoizedState) {
ReactCurrentDispatcher$1.current = HooksDispatcherOnUpdateInDEV;
} else {
ReactCurrentDispatcher$1.current = HooksDispatcherOnMountInDEV;
}
var children = Component(props, secondArg); // Execute the render function
ReactCurrentDispatcher$1.current = ContextOnlyDispatcher;
return children;
}ReactCurrentDispatcher$1.current is the object referenced when hooks are called. It is only set to the correct position before invoking the function; otherwise, an error function list is returned.

During hook calls, we get the latest state. This will be covered further in the hooks section, but for now, understand that we obtain the latest ReactElement object, which initiates the child node generation process in reconcileChildren, as previously analyzed. This process aims to create a Fiber node.
HostComponent
A function component generating child nodes typically produces a native DOM element node, classified as a HostComponent. Let’s look at the reconciliation flow for this type of node. This node, created during the reconciliation of a function component, is processed in updateHostComponent.
function updateHostComponent(current, workInProgress, renderLanes) {
var type = workInProgress.type;
var nextProps = workInProgress.pendingProps;
var prevProps = current ? current.memoizedProps : null; // For initialization, this is null
var nextChildren = nextProps.children;
reconcileChildren(current, workInProgress, nextChildren, renderLanes);
return workInProgress.child;
}The Fiber type for native DOM elements is simple, involving only the generation of child nodes, which directly enters the reconcileChildren step to start creating Fiber nodes.

The core of mountChildFibers is creating a single child Fiber node or a Fiber-linked list of child nodes.
Summary
During the initialization phase, beginWork mainly creates Fiber nodes, optimizes for function components, executes render functions, triggers hooks, calculates state, and tags nodes accordingly.
Update Scenario:
Fiber Reuse
Once our application has completed initialization, it enters the update phase. During updates, each node from the root proceeds through the beginWork process, but not every node needs a new fiber node creation. To optimize performance, React tries to reuse previous fiber nodes. Here’s how that happens:
function beginWork(current, workInProgress, renderLanes) {
if (current !== null) {
// Update phase
// Check if this node can be reused
if (canReuse) {
return reusableNode / clone;
}
}
// Behaves as in the mount phase
}Through a branch that filters out reusable nodes, React determines whether a node should be reused by checking specific conditions:
function checkScheduledUpdateOrContext(current, renderLanes) {
var updateLanes = current.lanes;
return includesSomeLane(updateLanes, renderLanes);
}Before entering beginWork, flags are set on the fiber node's ancestor nodes to indicate updates, allowing reuse for any nodes that don’t match this specific flag.

Single vs. Multiple Nodes
In the update phase, there are two possible scenarios for each level: a single node or multiple nodes. These cases are handled in reconcileChildFibers:
function reconcileChildFibers(returnFiber, currentFirstChild, newChild, lanes) {
if (typeof newChild === "object" && newChild !== null) {
switch (newChild.$$typeof) {
case REACT_ELEMENT_TYPE:
return placeSingleChild(
reconcileSingleElement( // Single node scenario
returnFiber,
currentFirstChild,
newChild,
lanes
)
);
}
if (isArray(newChild)) {
return reconcileChildrenArray( // Multiple nodes
returnFiber,
currentFirstChild,
newChild,
lanes
);
}
}
}If newChild is an array, reconcileChildrenArray is triggered, initiating the diff algorithm. We’ll explore the diff algorithm in a separate discussion. Here, we’ll focus on single-node reuse—how React reuses it through either cloning the original fiber or directly referencing it.
function reconcileSingleElement(returnFiber, currentFirstChild, element, lanes) {
var key = element.key;
var child = currentFirstChild;
while (child !== null) {
if (child.key === key) { // Same key
var elementType = element.type; // Tag check
if (child.elementType === elementType) { // Same tag
deleteRemainingChildren(returnFiber, child.sibling);
// Reuse the existing node
var _existing = useFiber(child, element.props);
_existing.ref = coerceRef(returnFiber, child, element);
_existing.return = returnFiber;
return _existing;
}
break;
}
child = child.sibling;
}
}Fiber reuse happens if two conditions are met:
Same key
Same tag
Reusing the fiber involves cloning it instead of directly referencing it:
function useFiber(fiber, pendingProps) {
// `fiber` is the old fiber node, `pendingProps` are the new props
var clone = createWorkInProgress(fiber, pendingProps);
clone.index = 0;
clone.sibling = null;
return clone;
}State Calculation
During updates, React recalculates the latest state based on the current one. This essential logic occurs in processUpdateQueue, which we’ll analyze step-by-step.
Step 1: beginWork retrieves the updateQueue, which, as previously discussed, is structured as a circular linked list.

Suppose there are four update tasks in this render: A1, B2, C1, and D2 (where "1" indicates high priority and "2" indicates low priority). The current render only processes high-priority updates.
Step 2: Split the list.

Step 3: Iterate through the list and calculate the state.

During iteration, only updates matching the current priority are calculated. Others are placed in firstBaseUpdate and lastBaseUpdate on the fiber for the next update. For example, C1’s priority is set to 0 for future processing. Therefore, the calculated state includes A and C.
When the next update priority is 2, it will traverse firstBaseUpdate, calculating based on baseState.

Summary
In the update phase, beginWork reuses nodes whenever possible, while state calculations skip lower-priority states, recalculating them in the next scheduling cycle.