Understanding React - Part 2.2. The Render and Commit stages of React’s Work LoopSeptember 7# Tech# Front-End# React
Introduction
In the last post, we introduced the structure of the Fiber tree and an overview of the React work loop. This time, we'll focus on the render and commit stages, the core of the React work loop. In short, during the render stage, React traverses the Fiber tree twice: first from the root to the leaves and then back up to create or update Fiber nodes and gather crucial information. In the commit stage, React applies these updates to the host environment, such as the DOM, and flushes effects. Together, these two stages form the backbone of React's reactivity, driving UI updates.
The following diagram illustrates this workflow. Let’s begin by diving into the render stage.
The Render Stage
The render stage is all about preparing the fiber tree—a lightweight representation of the UI. This stage is concerned with determining what changes need to be made to the UI but does not apply those changes directly to the DOM.
The render stage consists of two primary actions: begin work and complete work. React traverses the Fiber tree in two passes: first, it goes down the tree (begin work) and then returns up (complete work). Before we break down the render stage, let's first understand the concept of Update
.
Update
An Update
is the minimal unit of change that React processes. The basic structure of an Update
looks like this:
- action
Theaction
represents the new state or a function that computes the new state. If you’ve useduseState
, you might recognize this pattern:When callingsetCounter
, anUpdate
is created behind the scenes. You can pass a new value directly or a function that computes it based on the previous state.Although state updates are the most common use ofUpdate
, in React, virtually any change can trigger anUpdate
.
- next
Updates are linked together in a singly linked list. When a newUpdate
is appended, itsnext
property points to the subsequentUpdate
, making it easier for React to traverse them.
UpdateQueue
The linked list of Updates
is stored in an UpdateQueue
, which is part of a Fiber. Here’s a simplified structure:
The pending
property of the UpdateQueue
holds the head of the Update
linked list. React checks this queue to determine if there are any updates for that Fiber.
The Begin Work Phase
GitHub code reference: ReactFiberBeginWork.ts
In the begin work phase, React uses a depth-first search (DFS) strategy to traverse the Fiber tree. The main responsibilities of this phase are to process updates on each Fiber and to reconcile (create, update, or delete) child Fiber nodes.
Consider a simple React app:
Suppose we update the <App /> component to:
Here's a breakdown of what happens during the begin work phase:
When createRoot
is called, React creates and mounts a FiberRoot
and HostRoot
. The entry point, or root of the Fiber tree, is the HostRoot
Fiber. When the render
function is called, the <App />
component is encapsulated in an Update
and added to the updateQueue
of the HostRoot
. React then enters the work loop, starting from the HostRoot
.
During begin work:
- When mounting, React computes initial states and creates child Fiber nodes.
- When updating, React computes new states for each Fiber node. The
tag
property distinguishes between different Fiber types, affecting how updates are processed.
Let's look at the main types of Fibers and how they differ in the begin work phase. We'll cover child reconciliation in a separate post, as it's a complex topic.
For now, note that during reconciliation, React creates new Fibers or marks them for deletion. Only the root of a subtree of newly created Fibers is tagged with the Placement
effect. When a root Fiber is placed, the entire subtree is effectively added for performance reasons. For deletions, the root of the entire subtrees are stored in the deletions
property of the parent Fiber.
HostRoot
After calling render
following createRoot
, the <App />
component is stored in an Update
and added to the updateQueue
of the HostRoot
. During begin work in the HostRoot
, React processes the update and stores the new <App />
component in the memorizedState
of the HostRoot
. The child of HostRoot
becomes <App />
, and React begins creating child Fibers.
Function Component
Function components are pure functions that were initially stateless. With the introduction of hooks, we can now use useState
and other hooks to manage state in function components. We’ll discuss hooks in detail in a future post, but for now, keep in mind that when React processes begin work on function components, it handles hook updates to manage the Fiber's state.
The child of a function component is the return value of the function, usually a JSX element, which React processes next.
Fragment
Fragments help preserve tree structure when rendering an array of children. They are stateless.
The child of a Fragment is stored in the children
property of the pendingProps
on its Fiber, and React processes it in begin work.
HostComponent
Host components represent elements in the host environment (e.g., DOM elements like div
). These are stateless Fibers.
The child of a HostComponent is also stored in the children
property of the pendingProps
on its Fiber, if it exists, and React processes it in begin work.
HostText
HostText Fibers do not have state or children. Typically, they are leaf nodes in the Fiber tree and require no additional processing during begin work.
The Complete Work Phase
GitHub code reference: ReactFiberCompleteWork.ts
When the begin work phase reaches a leaf node, the complete work phase begins. React then traverses back up the Fiber tree until it finds a sibling node that has not yet processed begin work.
The complete work phase is lighter than begin work. Its primary tasks are to create or update host environment instances for host-related Fibers (e.g., HostComponent
and HostText
), flag updates, and propagate child effect flags up to their parent Fibers. This allows React to know if there are updates within a subtree.
When creating host instances, React appends child instances to parent instances, forming a tree. These instances are stored in the stateNode
property of the corresponding Fibers. At this point, these instances exist only in memory and will be rendered to the screen in the commit stage.
The Commit Stage
GitHub code reference: ReactFiberCommitWork.ts
Since React can run in environments other than the DOM, I’ll refer to elements likediv
or text nodes as instances and the DOM as the host environment.
Once the render stage is complete, React moves on to the commit stage. This is where the real DOM updates happen. React applies the changes calculated during the render stage to the actual DOM elements.
In the commit stage, React begins at the root of the Fiber tree and traverses downward to commit deletions, placements, and update effects.
During the initial render, the commit stage primarily handles placement, where React appends host nodes to the host environment. At this point, the UI is rendered to the screen.
For updates, React handles three main actions in sequence: deletions, updates, and placements.
First, React handles deletions, where it removes any nodes that are no longer needed from the host environment. This ensures that outdated elements are cleared, preventing memory leaks and keeping the UI up-to-date.
Next, React processes updates by comparing the current memorizedProps
with the previous ones and updating the host nodes accordingly. This phase ensures that the UI reflects the latest state by applying the necessary changes to DOM elements.
Finally, React manages placements, where new subtrees or nodes are inserted into the host environment. At this point, any new components or elements that were created during the render phase are placed into the DOM, making them visible on the screen.
After all the effects are committed, React swaps the current Fiber tree with the work-in-progress tree, and the changes are rendered to the screen.
Committing Effects
Beyond these DOM operations, the commit stage is also responsible for committing effects. Effects, such as those created by useEffect
and useLayoutEffect
, are flushed during this stage. This means that any side-effects your components rely on, like subscriptions, data fetching, or DOM mutations, are executed once the DOM has been updated.
For example, if you’ve used useEffect
to fetch data or manipulate the DOM, React schedules the effect to run after the UI has been updated. This ensures that your component interacts with the most up-to-date DOM and state. In the case of useLayoutEffect
, the effect is applied immediately after DOM mutations, but before the browser paints the UI.
This is a crucial part of React’s lifecycle, as it allows you to run logic that depends on the DOM’s state post-update. We’ll discuss hooks and effects in greater detail in a future post, as they form a significant part of how React manages side-effects in function components.
Conclusion
In this post, we’ve taken a detailed look at the render and commit stages of the React work loop, explaining how React traverses the Fiber tree to create and update the UI.
By understanding the render and commit stages, we can appreciate how React efficiently handles UI updates. The key lies in React’s ability to reconcile the current and future states of the UI, optimizing changes through the fiber architecture and the phased work loop.
The render stage prepares and plans the changes, while the commit stage executes them. With this knowledge, we can better troubleshoot performance issues and write more efficient React applications.
In the next post, we’ll dive into React’s diff algorithm and child reconciliation, a critical part of React's efficient UI updates.