It's already been several years since I started using React, and through many versions it has now reached v19. Every so often I'd think, "I should take a look at React's internal code," and I repeatedly attempted—and failed—to binge through the "React Deep Dive" series on the Deep Dive Magic Code | Goidle blog. This time I ended up studying with React Internals Deep Dive, and this time I want to keep the code close as I study and reorganize what I learn by category. While pondering how to study with the code close at hand, I set things up—similar to a monorepo—by cloning the react project and using its build output as a dependency in a project that builds a web page. This gave me an environment where I could use debugger and console handling in the react project and more directly observe the render and re-render logic through the browser's devtools Call Stack in the browser web page project.
React Deep Dive?
This is not a Deep Dive; it's simply an attempt to follow React's rendering lifecycle as observed in the setup above and organize the phases section by section. The reason for this organization is that, both when reading the earlier "React Deep Dive" series and during the study, I often got lost in the swamp of vast content and code, losing track of where in React I was currently looking… and what part of React I was learning. Dividing it into sections is meant to serve as a map (a signpost) so I don't lose my way in future studies. I'll organize the stages into the major divisions defined even on React's official site: Trigger, Render, Commit.
This is organized based on React v18.3.1, and afterward I want to compare how the Lifecycle Phase has changed in v19.
Note: organized based on React v18.3.1

Trigger
Let's start from render, the beginning of the very first render in a React project.
Based on the created root (ReactDOMRoot), the render call proceeds in the form below.
Functions I consider important, following the order of progression rather than depth
- render → updateContainer → scheduleUpdateOnFiber → ensureRootIsScheduled
render: receives the App component (ReactNodeList) and begins the update
tsxconst root = createRoot(document.getElementById("root"));
root.render(<App />);
tsxReactDOMHydrationRoot.prototype.render = ReactDOMRoot.prototype.render = function (children) {
var root = this._internalRoot;
...
updateContainer(children, root, null, null);
};
scheduleUpdateOnFiber: starts the rendering process; as the function that schedules the update, it identifies the Fiber node that needs to change and schedules the update.
tsxfunction updateContainer(element, container, parentComponent, callback) {
...
var root = enqueueUpdate(current$1, update, lane);
if (root !== null) {
scheduleUpdateOnFiber(root, current$1, lane, eventTime);
entangleTransitions(root, current$1, lane);
}
return lane;
}
ensureRootIsScheduled: called from within scheduleUpdateOnFiber during the update-scheduling process, it does the actual scheduling by scheduling the performConcurrentWorkOnRoot function, which performs the rendering work.
tsxfunction scheduleUpdateOnFiber(root, fiber, lane, eventTime) {
...
ensureRootIsScheduled(root, eventTime);
...
}
Through the process above, updates that require rendering are scheduled. Afterward, the scheduler schedules the performConcurrentWorkOnRoot function, which is the starting point of rendering, and the scheduled work is consumed sequentially through the workLoop loop.
Render
The updates scheduled in the Trigger stage are invoked through the postMessage of MessageChannel. The reason for using MessageChannel is that using setTimeout can incur a minimum 4ms delay on nested calls, which can affect performance; this reason is also stated in the comments within the code.
Functions I consider important, following my own sense of the order of progression rather than depth
- performUnitOfWork → beginWork → attemptEarlyBailoutIfNoScheduledUpdate → bailoutOnAlreadyFinishedWork → reconcileChildren → completeUnitOfWork → completeWork
performUnitOfWork: called through workLoop, it is the core render function that calls beginWork to process the work on a Fiber tree node.
tsxfunction performUnitOfWork(unitOfWork: Fiber): void {
...
next = beginWork(current, unitOfWork, subtreeRenderLanes);
...
completeUnitOfWork(unitOfWork);
...
}
beginWork: compares the current Fiber node with the new (workInProgress) Fiber node to perform work, and if there are no changes, it processes the bailout logic.
tsxfunction beginWork(
current: Fiber | null,
workInProgress: Fiber,
renderLanes: Lanes,
): Fiber | null {
...
if (current !== null) {
const oldProps = current.memoizedProps;
const newProps = workInProgress.pendingProps;
if (oldProps !== newProps) {
...
} else {
...
// No pending updates or context. Bail out now.
didReceiveUpdate = false;
return attemptEarlyBailoutIfNoScheduledUpdate(
current,
workInProgress,
renderLanes,
);
}
...
attemptEarlyBailoutIfNoScheduledUpdate: a function that attempts a bailout to optimize away unnecessary rendering when there are no updates.
tsxfunction attemptEarlyBailoutIfNoScheduledUpdate(
current: Fiber,
workInProgress: Fiber,
renderLanes: Lanes,
) {
...
return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
}
bailoutOnAlreadyFinishedWork: a function that performs the bailout logic, which stops rendering and no longer traverses the tree when there is no pending work in the subtree.
tsxfunction bailoutOnAlreadyFinishedWork(
current: Fiber | null,
workInProgress: Fiber,
renderLanes: Lanes,
): Fiber | null {
reconcileChildren: compares the new child nodes with the previous child nodes to create a new Fiber node tree.
tsxexport function reconcileChildren(
current: Fiber | null,
workInProgress: Fiber,
nextChildren: any,
renderLanes: Lanes,
) {
if (current === null) {
...
workInProgress.child = mountChildFibers(...
} else {
...
workInProgress.child = reconcileChildFibers(...
}
}
completeUnitOfWork: when beginWork() did not create child nodes, it completes the work on the current Fiber node and, if necessary, creates DOM nodes or marks updates.
tsxfunction completeUnitOfWork(unitOfWork: Fiber): void {
...
do {
...
const current = completedWork.alternate;
const returnFiber = completedWork.return;
if ((completedWork.flags & Incomplete) === NoFlags) {
...
completeWork(current, completedWork, subtreeRenderLanes);
} else {
...
unwindWork(current, completedWork, subtreeRenderLanes);
...
}
...
workInProgress = completedWork;
} while (completedWork !== null);
completeWork: depending on the Fiber node's tag, it creates or updates a DOM node and sets the necessary effect flags.
tsxfunction completeWork(
current: Fiber | null,
workInProgress: Fiber,
renderLanes: Lanes,
): Fiber | null {
...
switch (workInProgress.tag) {
...
}
Scheduled updates are processed by traversing the Fiber tree. In this process, during the Reconciliation stage, React compares the previous tree with the new tree and, by applying optimization techniques such as Bailout, identifies only the work that actually needs to change. The work selected this way is then collected into an Effect list to be processed later.
Commit
Once the Render Phase is complete, React enters the Commit Phase, which reflects the computed results into the actual browser environment. In this stage, based on the finalized Fiber tree, DOM manipulation, component lifecycle handling, and execution of registered Effects are performed.
Functions I consider important, following my own sense of the order of progression rather than depth
- commitRoot → commitMutationEffectsOnFiber → flushPassiveEffects → commitPassiveMountOnFiber
commitRoot: processes all passive effects, completes all render processes, and begins the commit stage.
tsxfunction commitRoot(
root: FiberRoot,
recoverableErrors: null | Array<CapturedValue<mixed>>,
transitions: Array<Transition> | null,
) {
...
commitRootImpl(
root,
recoverableErrors,
transitions,
previousUpdateLanePriority,
);
...
}
commitMutationEffectsOnFiber: begins the stage that handles mutation effects such as insertion (Placement), deletion (ChildDeletion), and update (Update) (the core function that performs actual DOM manipulation based on the render tree).
tsxfunction commitMutationEffectsOnFiber(
finishedWork: Fiber,
root: FiberRoot,
lanes: Lanes,
) {
...
recursivelyTraverseMutationEffects(root, finishedWork, lanes);
commitReconciliationEffects(finishedWork);
...
commitUpdate(
instance,
updatePayload,
type,
oldProps,
newProps,
finishedWork,
);
...
}
flushPassiveEffects: executes the Passive effects that run asynchronously after the DOM update, and runs the cleanup functions when components unmount.
tsxexport function flushPassiveEffects(): boolean {
...
return flushPassiveEffectsImpl();
...
}
function flushPassiveEffectsImpl() {
...
commitPassiveUnmountEffects(root.current);
commitPassiveMountEffects(root, root.current, lanes, transitions);
...
}
commitPassiveMountOnFiber: executes the Passive mount effects for a Fiber node and handles different kinds of Passive effects depending on the Fiber node type. It compares caches and updates only when an update is needed.
tsxfunction commitPassiveMountOnFiber(
finishedRoot: FiberRoot,
finishedWork: Fiber,
committedLanes: Lanes,
committedTransitions: Array<Transition> | null,
): void {
...
commitHookEffectListMount(HookPassive | HookHasEffect, finishedWork);
...
}
To summarize, the Commit Phase begins after all rendering work is complete, and it first cleans up and runs the previously registered passive effects. Next, the work of actually inserting or deleting the DOM elements that need changes, and updating their attribute values, is performed. These DOM changes and side-effect handling are the final step in reflecting the changes onto the screen, forming the result visible to the user.
In Conclusion
I've organized this very briefly, dividing it into the Trigger, Render, and Commit stages, and as I said earlier, this article is a kind of 'map' I'm making so I don't lose my way while learning React—written with the sense of looking at the forest first before looking at the trees. For those reading this, I encourage you to keep in mind the possibility that this organized 'forest' may be wrong, and if you want to know more deeply about how React works, to study React Internals Deep Dive or analyze the source code yourself.
What I felt while putting this together was that, as I organized things while debugging React code, it felt like drawing a map by hand and finding my way through a maze. I hope that someday this work of wandering through mazes and drawing maps will become familiar, and that when I encounter the next maze, I'll be able to complete the map quickly and find my way out.

"The creation of a thousand forests is in one acorn." — Ralph Waldo Emerson