We’ve finally reached the render phase, where we delve into how React displays the UI and processes state changes—the core functionality of React. But how can we definitively determine if we’re in the render phase? While opinions vary, we’ll clarify this concept here.
In the React source code, the state is represented using the global variable executionContext:
export const NoContext = /* */ 0b000; // 0 const BatchedContext = /* */ 0b001; // 1 const RenderContext = /* */ 0b010; // 2 const CommitContext = /* */ 0b100; // 4
For this article, we adopt the mainstream definition: any process where executionContext includes RenderContext is considered the render phase. In previous sections, we explored priority concepts and learned that React schedules asynchronous tasks through the Scheduler. This task, essentially a function named performConcurrentWorkOnRoot or performSyncWorkOnRoot, is responsible for executing tasks, and this article focuses on the render phase within these functions.
Before the Render Phase
In the previous article, “Understanding React: Prioritization (Part 2),” we discussed that the task scheduled for execution is performConcurrentWorkOnRoot. However, it doesn’t immediately enter the render process. Let’s see what happens beforehand:
function performConcurrentWorkOnRoot(root, didTimeout) {
// didTimeout is provided by the Scheduler; it returns false if there are remaining time slices, true if not.
...
var lanes = getNextLanes(
root,
root === workInProgressRoot ? workInProgressRootRenderLanes : NoLanes
);
if (lanes === NoLanes) {
return null;
}
var shouldTimeSlice =
!includesBlockingLane(root, lanes) &&
!includesExpiredLane(root, lanes) &&
!didTimeout;
var exitStatus = shouldTimeSlice
? renderRootConcurrent(root, lanes)
: renderRootSync(root, lanes);
// The rest of this handles the commit phase, which we’ll cover later.
...
return null;
}The function first obtains the current task priority, which informs subsequent state calculations. Even when performConcurrentWorkOnRoot is called, concurrent rendering is not guaranteed; it depends on certain conditions based on the task's priority.
Conditions for Concurrent Rendering
To enter concurrent rendering mode, three conditions must be met:
Task Priority Excludes
includesBlockingLanePriorityfunction includesBlockingLane(root, lanes) { var SyncDefaultLanes = InputContinuousHydrationLane | InputContinuousLane | DefaultHydrationLane | DefaultLane; return (lanes & SyncDefaultLanes) !== NoLanes; }The task priority must not include
SyncDefaultLanes.Task Priority Is Not Expired
function includesExpiredLane(root, lanes) { return (lanes & root.expiredLanes) !== NoLanes; }This check returns
trueif the task priority isn’t inroot.expiredLanes.Scheduler Has Remaining Time Slices
The Scheduler must still have available time slices.
If it’s the initial render, it defaults to synchronous rendering since the priority is DefaultLane, which doesn’t meet the first condition.

In such cases, we proceed to renderRootSync, which signifies the start of the render phase from the root.
Render Phase
Next, the execution flow enters renderRootSync.
function renderRootSync(root, lanes) {
var prevExecutionContext = executionContext;
executionContext |= RenderContext; // Mark the render phase
var prevDispatcher = pushDispatcher();
if (
workInProgressRoot !== root ||
workInProgressRootRenderLanes !== lanes
) {
// Set transition priority
workInProgressTransitions = getTransitionsForLanes();
// Prepare the workInProgress stack and set Update on shared.pending
prepareFreshStack(root, lanes);
}
// Begin synchronous render
do {
try {
workLoopSync();
break;
} catch (thrownValue) {
handleError(root, thrownValue);
}
} while (true);
// Reset context and other global variables
resetContextDependencies();
executionContext = prevExecutionContext;
popDispatcher(prevDispatcher);
workInProgressRoot = null;
workInProgressRootRenderLanes = NoLanes;
return workInProgressRootExitStatus;
}Step 1:
The line executionContext |= RenderContext officially marks the flow as in the render phase.
Step 2:
The dispatcher is prepared. This dispatcher refers to the useState and similar hooks called through the global variable ReactCurrentDispatcher$2.current. By assigning different objects to it, we can call different APIs. This mechanism prevents hooks from being used outside components, as doing so causes errors because of the dispatcher configuration.

For the initial mount, workInProgressRoot will be empty, so an empty workInProgress is prepared. The render process essentially generates the latest ReactElements based on the most recent state, then calculates differences with the current tree to mark the changes. Here’s what prepareFreshStack does:
function prepareFreshStack(root, lanes) {
root.finishedWork = null;
root.finishedLanes = NoLanes;
if (workInProgress !== null) {
...
}
// Assign values to global variables related to workInProgress
workInProgressRoot = root;
// Create an empty workInProgress tree
var rootWorkInProgress = createWorkInProgress(root.current, null);
// Set global variables related to workInProgress
workInProgress = rootWorkInProgress;
workInProgressRootRenderLanes = subtreeRenderLanes = workInProgressRootIncludedLanes = lanes;
workInProgressRootExitStatus = RootInProgress;
...
finishQueueingConcurrentUpdates();
return rootWorkInProgress;
}The purpose here is to prepare workInProgress. createWorkInProgress clones the current tree into a new Fiber node:
function createWorkInProgress(current, pendingProps) {
var workInProgress = current.alternate;
if (workInProgress === null) {
workInProgress = createFiber(
current.tag,
pendingProps,
current.key,
current.mode
);
workInProgress.elementType = current.elementType;
workInProgress.type = current.type;
workInProgress.stateNode = current.stateNode;
workInProgress.alternate = current;
current.alternate = workInProgress;
} else {
workInProgress.pendingProps = pendingProps;
workInProgress.type = current.type;
workInProgress.flags = NoFlags;
workInProgress.subtreeFlags = NoFlags;
workInProgress.deletions = null;
}
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;
...
return workInProgress;
}Now, the memory contains this structure, and we proceed with constructing the workInProgress tree. Initially, this executes synchronously via workLoopSync:

function workLoopSync() {
while (workInProgress !== null) {
performUnitOfWork(workInProgress);
}
}Since the first workInProgress node is prepared, this loop will execute until the entire tree has been reconciled.
function performUnitOfWork(unitOfWork) {
var current = unitOfWork.alternate;
var next;
// Start reconciliation
next = beginWork(current, unitOfWork, subtreeRenderLanes);
unitOfWork.memoizedProps = unitOfWork.pendingProps;
if (next === null) {
// Complete unit work
completeUnitOfWork(unitOfWork);
} else {
workInProgress = next;
}
ReactCurrentOwner$2.current = null;
}The Render Phase Breakdown
Every Fiber node undergoes beginWork and completeWork. Suppose we have the following DOM structure:
const App = () => {
return (
<div>
<p>
<span>hello</span>
</p>
<span>Understanding React in Depth</span>
</div>
)
}The reconciliation process unfolds as follows:
beginWork: RootFiberbeginWork: AppbeginWork: divbeginWork: pbeginWork: span-hellocompleteWork: span-hellocompleteWork: pbeginWork: span-Understanding React in DepthcompleteWork: span-Understanding React in DepthcompleteWork: divcompleteWork: AppcompleteWork: RootFiber
The reconciliation ends here.

Two Trees
Throughout the React runtime, two Fiber trees always exist in memory. The task of the render phase is to build a workInProgress tree based on the current current tree, which will be the next to render.
Initialization
During the mount phase (when the page loads from scratch), React establishes a workInProgress tree through the initialization process.

After going through the render process, this structure exists in memory. React uses workInProgress to render the actual DOM interface, and then points the current pointer to this workInProgress.

Update
In the update phase, a new workInProgress tree is reconstructed in memory based on the current current tree. This process aims to reuse as many of the previous Fiber nodes as possible. After completing the render process, the current pointer updates to the new workInProgress tree.
