Understanding React - Part 2.2. The Render and Commit stages of React’s Work Loop

Understanding React - Part 2.2. The Render and Commit stages of React’s Work Loop
September 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.

diagram of work loop

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:

update type
  • action
    The action represents the new state or a function that computes the new state. If you’ve used useState, you might recognize this pattern:When calling setCounter, an Update 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 of Update, in React, virtually any change can trigger an Update.
An useState example
  • next
    Updates are linked together in a singly linked list. When a new Update is appended, its next property points to the subsequent Update, 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:

An update queue

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:

Diagram of the begin work

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.

Diagram of the complete work

The Commit Stage

GitHub code reference: ReactFiberCommitWork.ts

Since React can run in environments other than the DOM, I’ll refer to elements like div 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.

Diagram of the commit stage

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.

Related Reads