Establishing the Context
React operates as a JavaScript library centered on constructing user interfaces. Its fundamental purpose involves a mechanism that identifies alterations in component state and reflects those changes onto the display. Within React, this operation is referred to as reconciliation. When you invoke setState, the framework verifies whether state or props have changed and subsequently re-renders the component across the UI.
The official React documentation offers a solid high-level explanation of this process: the significance of React elements, lifecycle methods alongside the render method, and the diffing algorithm applied to child components. The structure of immutable React elements generated by the render method frequently bears the label "virtual DOM." This phrase proved useful for introducing React to newcomers, yet it also generated ambiguity and has since been abandoned in the official React docs. Throughout this piece, I will refer to it simply as the React element tree.
In addition to the React element tree, the framework has consistently maintained a separate structure of internal instances—including components and DOM nodes—responsible for preserving state. Beginning with version 16, React introduced an alternative implementation for this internal instance tree, along with a managing algorithm known by the codename Fiber. To understand the benefits offered by the Fiber design, refer to The how and why on React’s usage of linked list in Fiber.
This piece marks the starting point of a series devoted to explaining React's internal architecture. My goal here is to deliver a thorough examination of the core concepts and data structures tied to the algorithm. Once that foundation exists, we will investigate the algorithm itself and the primary functions used for navigating and handling the fiber tree. Following articles in this series will illustrate how React leverages the algorithm to execute the initial render and manage state and prop transitions. From that point, we will dive into the scheduler specifics, the reconciliation process for children, and the creation of the effects list.
The knowledge I’m about to share is fairly advanced ?. Take the time to read through it carefully if you want to grasp the inner magic behind Concurrent React. If contributing to React is on your radar, this series will act as a valuable companion. Coming from a background that values reverse-engineering, I will frequently link to the source code taken from version 16.6.0.
Bear in mind that this is substantial material, so don't be discouraged if certain parts don't click right away. Mastery takes time, as is true for anything worthwhile. It’s important to note that none of this is required knowledge for using React; this material focuses on how React functions underneath.
Foundational Setup
For illustration throughout this series, I’m working with a basic application. It includes a button that increments a displayed numeric value:

And here’s the code for it:
class ClickCounter extends React.Component {
constructor(props) {
super(props);
this.state = {count: 0};
this.handleClick = this.handleClick.bind(this);
}
handleClick() {
this.setState((state) => {
return {count: state.count + 1};
});
}
render() {
return [
<button key="1" onClick={this.handleClick}>Update counter</button>,
<span key="2">{this.state.count}</span>
]
}
}
Feel free to experiment with it here. The structure is straightforward: a single component that produces two child elements, button and span, from its render method. Clicking the button triggers a state update within the handler. That action leads to a text modification inside the span element.
Reconciliation involves numerous tasks performed by React. Consider these high-level actions React executes during the initial render and after a state change in our sample app:
- adjusts the
countproperty withinClickCounter'sstate - retrieves and evaluates children of
ClickCounteralong with their props - applies new props to the
spanelement
Reconciliation also encompasses other actions such as triggering lifecycle methods or refreshing refs. In the Fiber architecture, all of these tasks fall under the umbrella term "work." The nature of that work is typically determined by the type of React element involved. For instance, class components require React to create an instance, whereas functional components do not. React supports various element varieties—class and functional components, host components tied to DOM nodes, portals, and others. The element type gets established by the first argument passed to the createElement function, which is typically utilized within the render method to build elements.
Before we start looking into these activities and the core fiber algorithm, it's wise to first become acquainted with the internal data structures that React relies on.
Transforming React Elements into Fiber Nodes
In React, each component's UI representation is a view or template produced by the render method. For our ClickCounter component, that template looks like this:
<button key="1" onClick={this.onClick}>Update counter</button>
<span key="2">{this.state.count}</span>
React Elements
When a template is processed by the JSX compiler, it yields a collection of React elements. These elements, not HTML, are the actual return value of a component's render method. Since JSX is optional, the render method for ClickCounter could alternatively be expressed as:
class ClickCounter {
...
render() {
return [
React.createElement(
'button',
{
key: '1',
onClick: this.onClick
},
'Update counter'
),
React.createElement(
'span',
{
key: '2'
},
this.state.count
)
]
}
}
Invoking React.createElement within the render method results in two distinct data structures:
[
{
$$typeof: Symbol(react.element),
type: 'button',
key: "1",
props: {
children: 'Update counter',
onClick: () => { ... }
}
},
{
$$typeof: Symbol(react.element),
type: 'span',
key: "2",
props: {
children: 0
}
}
]
Notice that React tags these objects with the [$$typeof](https://overreacted.io/why-do-react-elements-have-typeof-property/) property to mark them as React elements uniquely. The object then carries type, key, and props properties, populated from the arguments supplied to React.createElement. React encodes text content as children of the span and button nodes, and the click handler lives within the button element's props. Additional fields, such as ref, exist on React elements but fall outside this discussion.
For the ClickCounter element, there are no props or keys:
{
$$typeof: Symbol(react.element),
key: null,
props: {},
ref: null,
type: ClickCounter
}
Fiber Nodes
During reconciliation, the data from each React element produced by render is integrated into a tree of fiber nodes. Every React element gets a matching fiber node. In contrast to React elements, fibers are not regenerated with each render; they are mutable structures that carry component state and DOM references.
As previously mentioned, the framework must perform different operations depending on the React element's type. In our example, the class component ClickCounter triggers lifecycle methods and the render method, while the span host component (a DOM node) requires DOM mutations. To manage this, each React element is transformed into a specific type of fiber node that outlines the required work.
A fiber can be viewed as a data structure representing a task or unit of work. The fiber architecture simplifies tracking, scheduling, pausing, and canceling these tasks.
On the initial conversion of a React element to a fiber node, React builds the fiber using data from the element within the createFiberFromTypeAndProps function. For subsequent updates, React reuses the existing fiber node and modifies only the necessary properties based on the new React element. Depending on the key prop, React might reposition the node within the hierarchy or remove it entirely if its corresponding element is absent from the latest render output.
Examine the ChildReconciler function to see all operations and associated functions React applies to existing fiber nodes.
React generates a fiber for each React element, and since elements form a tree, the fibers also form a tree. For our sample application, this tree appears as follows:

Fiber nodes are interconnected via a linked list using the child, sibling, and return properties. For a deeper explanation of this design, refer to my article The how and why on React’s usage of linked list in Fiber, if you haven't read it already.
Current and Work-in-Progress Trees
Following the initial render, React maintains a fiber tree that mirrors the application state used for the rendered UI. This is commonly known as the current tree. When updates are initiated, React constructs a workInProgress tree that represents the future state destined for the screen.
All processing happens on fibers within the workInProgress tree. As React traverses the current tree, it generates an alternate node for each existing fiber, forming the basis of the workInProgress tree. These new nodes use data from the React elements produced by the render method. After updates are processed and all work finishes, React has an alternate tree ready for display. Once rendered on screen, this workInProgress tree becomes the new current tree.
A fundamental principle in React is consistency. React updates the DOM in a single pass, avoiding any intermediate displays. The workInProgress tree acts as an invisible draft, enabling React to process all components first, then commit all changes to the screen at once.
In the source code, many functions operate on fibers from both the current and workInProgress trees. Here’s an example function signature:
function updateHostComponent(current, workInProgress, renderExpirationTime) {...}
Every fiber node has an alternate field pointing to its counterpart in the other tree. Nodes in the current tree reference nodes in the workInProgress tree, and the reverse is also true.
Side-effects
Think of a React component as a function that derives its UI from state and props. Any other activity—like DOM updates or lifecycle method invocations—qualifies as a side-effect, or simply an effect. The official docs also discuss this:
You’ve likely performed data fetching, subscriptions, or manually changing the DOM from React components before. We call these operations “side effects” (or “effects” for short) because they can affect other components and can’t be done during rendering.
Most state and prop updates inevitably lead to side-effects. Since effects represent work, a fiber node serves as an ideal mechanism for tracking them alongside updates. Each fiber node can carry associated effects, encoded in the effectTag field.
In Fiber, effects specify the work required for instances once updates are done. For host components (DOM elements), this covers adding, updating, or deleting elements. For class components, React might update refs and call componentDidMount and componentDidUpdate. Other effects apply to different fiber types.
Effects List
React handles updates rapidly, relying on a few clever techniques for performance. A notable one is building a linear list of fiber nodes with effects for rapid iteration. Traversing this linear list is far more efficient than a tree, and it avoids spending time on nodes without side-effects.
This list identifies nodes needing DOM updates or other effects. It is a subset of the finishedWork tree, linked via the nextEffect property, unlike the child property used in current and workInProgress trees.
Dan Abramov once compared the effects list to a Christmas tree, with “Christmas lights” stringing together all effectful nodes. To illustrate, consider a fiber tree where certain nodes require work. Suppose our update inserts c2 into the DOM, changes attributes on d2 and c1, and triggers a lifecycle method on b2. The effect list links these nodes so React can skip the rest:

See how nodes with effects are linked. React uses the firstEffect pointer to locate the start of the list. The diagram above becomes a linear sequence like this:

Root of the Fiber Tree
Every React app contains at least one DOM element acting as a container. In our case, it’s the div with the ID container.
const domContainer = document.querySelector('#container');
ReactDOM.render(React.createElement(ClickCounter), domContainer);
React creates a fiber root object for each container. This object is reachable via the DOM element reference:
const fiberRoot = query('#container')._reactRootContainer._internalRoot
The fiber root holds the reference to the fiber tree, stored in its current property:
const hostRootFiberNode = fiberRoot.current
The tree begins with a special fiber type called HostRoot, created internally to act as the parent of your top-level component. The HostRoot fiber connects back to the FiberRoot via the stateNode property:
fiberRoot.current.stateNode === fiberRoot; // true
To inspect the fiber tree, access the topmost HostRoot fiber node through the fiber root. Alternatively, obtain an individual fiber node from a component instance like this:
compInstance._reactInternalFiber
Fiber Node Structure
Now let's examine the fiber node structures for the ClickCounter component:
{
stateNode: new ClickCounter,
type: ClickCounter,
alternate: null,
key: null,
updateQueue: null,
memoizedState: {count: 0},
pendingProps: {},
memoizedProps: {},
tag: 1,
effectTag: 0,
nextEffect: null
}
and the span DOM element:
{
stateNode: new HTMLSpanElement,
type: "span",
alternate: null,
key: "2",
updateQueue: null,
memoizedState: null,
pendingProps: {children: 0},
memoizedProps: {children: 0},
tag: 5,
effectTag: 0,
nextEffect: null
}
The fiber nodes contain numerous fields. I've explained alternate, effectTag, and nextEffect in earlier sections. Here's what the remaining fields do:
stateNode
This holds the reference to the component's class instance, a DOM node, or other React element type tied to the fiber. Essentially, it stores the local state associated with the fiber.
type
This defines the function or class linked to the fiber. For class components, it points to the constructor; for DOM elements, it specifies the HTML tag. I often rely on this field to identify what element a fiber corresponds to.
tag
This defines the fiber type, crucial for the reconciliation algorithm to decide required work. Different React element types need different work, as noted before. The createFiberFromTypeAndProps function maps each React element to a fiber type. In our app, ClickCounter has a tag of 1 (a ClassComponent), while span has 5 (a HostComponent).
updateQueue
A queue holding state updates, callbacks, and DOM updates.
memoizedState
The fiber's state used to generate its output. During updates, it reflects the state currently on screen.
memoizedProps
The props used to create the output in the previous render.
pendingProps
Props updated with new data from React elements, pending application to child components or DOM elements.
key
A unique identifier within a group of children that helps React determine which items changed, were added, or removed. This ties into React's “lists and keys” feature detailed here.
The full fiber node structure is available here. I've left out several fields above, notably the child, sibling, and return pointers comprising the tree structure, which I detailed in my previous article. There's also the set of fields—expirationTime, childExpirationTime, and mode—that are specific to the Scheduler.
High-Level Workflow
React’s architecture splits the workflow into two distinct stages: the render phase and the commit phase.
In the render phase, React processes any updates that have been scheduled via methods like setState or React.render, and determines precisely what needs to change in the user interface. On the very first render, React constructs a new fiber node for every element returned by the render method. On subsequent renders, it reuses and refreshes the fibers associated with existing elements. The final output of this stage is a complete fiber tree where each node carries a list of side-effects. These effects specify the actual work that the commit phase will carry out.
The commit phase is where the rubber meets the road. React walks through the fiber tree, applies all the recorded effects to the component instances, and performs updates to the DOM or any other user-visible mutations.
A crucial aspect of the render phase is its asynchronous nature. React can process one fiber node or several depending on the available time slice, then pause entirely to handle other events. When it resumes, it picks up exactly where it left off. There are also scenarios where React must discard the work already done and restart from scratch. What makes these pauses possible is that the render phase never makes changes that are visible to the user. The commit phase, conversely, is strictly synchronous because the operations it performs directly affect the user interface. React's goal is to execute all those changes in one continuous, uninterrupted pass.
A significant portion of the work that React performs includes invoking lifecycle methods. Some of these are called during the render phase, while others are exclusive to the commit phase. The following methods are invoked during render:
- [UNSAFE_]componentWillMount (deprecated)
- [UNSAFE_]componentWillReceiveProps (deprecated)
- getDerivedStateFromProps
- shouldComponentUpdate
- [UNSAFE_]componentWillUpdate (deprecated)
- render
Notice that certain legacy methods from the render phase have been branded with the UNSAFE marker since version 16.3. The official documentation now refers to these as legacy lifecycles. The plan is to remove them in future 16.x releases, with the non-prefixed versions being fully eliminated in version 17.0. For a deeper explanation and a migration guide, follow this link.
Why is this change being made?
We established earlier that the render phase is asynchronous because it doesn't cause side-effects like DOM alterations. This opens the door for React to process updates out of order or across multiple threads. But the UNSAFE methods have historically been misunderstood. Developers often placed side-effect-heavy code inside them, which is problematic for async rendering. While only the methods without the UNSAFE prefix will be eliminated, even the prefixed versions carry significant risk in the upcoming Concurrent Mode (though you can opt out of it).
The lifecycle methods listed below are reserved for the commit phase:
- getSnapshotBeforeUpdate
- componentDidMount
- componentDidUpdate
- componentWillUnmount
Since these execute in the synchronous commit phase, they can safely perform side-effects and manipulate the DOM.
With this context, we can now delve into the generic algorithm that traverses the tree and handles the workload.
Render Phase Mechanics
The reconciliation process is kickstarted from the root HostRoot fiber via the renderRoot function. However, React doesn't re-process every node. It speedily "bails out" of parents that have already been handled, skipping straight to the fiber with pending work. For instance, if you trigger setState in a deeply nested component, React will start at the top but blur through the higher-level nodes to reach the specific component where the update was called.
Work Loop Essentials
The processing of fiber nodes occurs within the work loop. Here’s the synchronous variant of that loop’s implementation:
function workLoop(isYieldy) {
if (!isYieldy) {
while (nextUnitOfWork !== null) {
nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
}
} else {...}
}
The variable nextUnitOfWork in the code acts as a pointer to the current fiber node in the workInProgress tree that still needs processing. As React navigates the fiber tree, this variable tracks whether more work awaits. Once a fiber is finished, nextUnitOfWork either holds a reference to the subsequent node or becomes null. A null value signals that the loop is finished and React can move to the commit stage.
Tree traversal and task initiation rely on four core functions:
To illustrate how these fit together, check out the animation below depicting a traversal of a fiber tree. This demo uses a simplified rendition of the functions, and you can observe the active fiber node change as React descends. Watch closely how the algorithm flits between branches: it always finalizes the children before moving onward to the parents.

Bear in mind that vertical links indicate siblings, whereas curved links point to children—for example,
b1has no children butb2does, i.e.,c1.
You can view the original video here to pause and examine the active node and function states. Conceptually, "begin" equates to entering a component, while "complete" signals leaving it. For hands-on experimentation, you can play with the example and code as we break down each function.
We’ll start by looking at performUnitOfWork and beginWork:
function performUnitOfWork(workInProgress) {
let next = beginWork(workInProgress);
if (next === null) {
next = completeUnitOfWork(workInProgress);
}
return next;
}
function beginWork(workInProgress) {
console.log('work performed for ' + workInProgress.name);
return workInProgress.child;
}
performUnitOfWork takes a fiber from the workInProgress tree and initiates the work by calling beginWork. This is the function responsible for executing all the tasks tied to that specific fiber. In this simplified illustration, we just log the fiber's name to signify activity. Crucially, beginWork returns either a pointer to the next child that needs handling, or null if there are none.
If the function yields a child, the workLoop assigns it to nextUnitOfWork. But in the absence of a child, React knows it’s hit the end of a branch and can mark the current node as complete. After completion, React must tackle the node’s siblings and then trace back to its parent. This process unfolds in completeUnitOfWork:
function completeUnitOfWork(workInProgress) {
while (true) {
let returnFiber = workInProgress.return;
let siblingFiber = workInProgress.sibling;
nextUnitOfWork = completeWork(workInProgress);
if (siblingFiber !== null) {
// If there is a sibling, return it
// to perform work for this sibling
return siblingFiber;
} else if (returnFiber !== null) {
// If there's no more work in this returnFiber,
// continue the loop to complete the parent.
workInProgress = returnFiber;
continue;
} else {
// We've reached the root.
return null;
}
}
}
function completeWork(workInProgress) {
console.log('work completed for ' + workInProgress.name);
return null;
}
This function revolves around a sizeable while loop. React enters it once a workInProgress node has no children. After wrapping up the current fiber, it checks for a sibling. If one exists, React exits the function, returning the sibling as the next node for nextUnitOfWork. The loop then resumes with that sibling's branch. Note that finishing work on a sibling doesn't mean the parent is done. The parent fiber only completes after all the branches descending from it have been fully processed, at which point React backtracks.
From the implementation above, it’s clear that completeUnitOfWork and performUnitOfWork handle the iteration logic, while the heavy lifting occurs inside beginWork and completeWork. In the upcoming sections of this series, we’ll explore what happens to the ClickCounter component and the span node as React steps through these functions.
Commit phase execution
The process kicks off with completeRoot, where React synchronizes the DOM and triggers lifecycle methods both before and after mutations.
At this stage, React holds two trees along with the effects list. One tree corresponds to the currently visible state on screen. The other, referred to as finishedWork or workInProgress in the source code, is constructed during the render phase and embodies the state that should now be displayed. These two trees share a similar structure, connected via child and sibling pointers.
Additionally, there is the effects list — a collection of nodes extracted from the finishedWork tree, linked through the nextEffect pointer. It is essential to note that this list is the outcome of the render phase. The purpose of rendering was to identify which nodes require insertion, modification, or removal, and which components need lifecycle method invocations. The effects list captures exactly that, and it is precisely this set of nodes that is traversed during the commit phase.
For debugging, you can reach the
currenttree via thecurrentproperty on the fiber root, while thefinishedWorktree is accessible through thealternateproperty on theHostFibernode of the current tree.
The central function executing this phase is commitRoot, which performs the following tasks in order:
- Triggers
getSnapshotBeforeUpdateon nodes carrying theSnapshoteffect - Invokes
componentWillUnmountfor nodes marked with theDeletioneffect - Carries out all DOM insertions, updates, and removals
- Promotes the
finishedWorktree to become the current tree - Calls
componentDidMounton nodes flagged with thePlacementeffect - Executes
componentDidUpdateon nodes flagged with theUpdateeffect
Following the pre-mutation hook getSnapshotBeforeUpdate, React applies all side effects in the tree through a two-pass sequence. The initial pass handles all DOM-related insertions, updates, removals, and ref detachments. Afterward, React assigns the finishedWork tree to the FiberRoot, designating the workInProgress tree as the new current. This switch happens between the two passes — ensuring the old tree remains current during componentWillUnmount, yet the fresh tree is active before componentDidMount or componentDidUpdate run. The second pass then invokes the remaining lifecycle methods and ref callbacks. This separation allows all placements, updates, and deletions across the entire tree to be completed first.
Below is a condensed version of the function driving these steps:
function commitRoot(root, finishedWork) {
commitBeforeMutationLifecycles()
commitAllHostEffects();
root.current = finishedWork;
commitAllLifeCycles();
}
Each of these sub-functions traverses the effects list, inspecting effect types, and applies the relevant operation whenever a match is found.
Lifecycle methods before mutations
As an illustration, here is the loop that scans the effects tree for the Snapshot effect:
function commitBeforeMutationLifecycles() {
while (nextEffect !== null) {
const effectTag = nextEffect.effectTag;
if (effectTag & Snapshot) {
const current = nextEffect.alternate;
commitBeforeMutationLifeCycles(current, nextEffect);
}
nextEffect = nextEffect.nextEffect;
}
}
For class components, encountering this effect means invoking the getSnapshotBeforeUpdate lifecycle method.
Applying DOM changes
In commitAllHostEffects, React carries out all DOM manipulations. This function determines the required action for each node based on its effect type and then executes it:
function commitAllHostEffects() {
switch (primaryEffectTag) {
case Placement: {
commitPlacement(nextEffect);
...
}
case PlacementAndUpdate: {
commitPlacement(nextEffect);
commitWork(current, nextEffect);
...
}
case Update: {
commitWork(current, nextEffect);
...
}
case Deletion: {
commitDeletion(nextEffect);
...
}
}
}
Notably, React integrates the componentWillUnmount call within the deletion routine found in commitDeletion.
Lifecycle methods after mutations
The remaining lifecycle hooks — componentDidUpdate and componentDidMount — are triggered within commitAllLifecycles.
That wraps it up. Feel free to share your thoughts or pose questions in the comments. Be sure to continue with the upcoming article in this series: In-depth explanation of state and props update in React. Additional articles are on the way, covering the scheduler, the children reconciliation algorithm, and the construction of the effects list. A video tutorial demonstrating how to debug applications using the insights from this piece is also planned.
