I’ve outlined main data structure and concepts that I’ll be using in this article, particularly Fiber nodes, current and work-in-progress trees, side-effects and the effects list. I’ve also provided a high-level overview of the main algorithm and explained the difference between the render and commit phases. If you haven’t read it, I recommend that you start there.

I’ve also introduced you to the sample application with a button that simply increments a number rendered on the screen:

You can play with it here. It’s implemented as a simple component that returns two child elements button and span from the render method. As you click on the button, the state of the component is updated inside the handler. This results in the text update for the span element:

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};
        });
    }
    
    componentDidUpdate() {}

    render() {
        return [
            <button key="1" onClick={this.handleClick}>Update counter</button>,
            <span key="2">{this.state.count}</span>
        ]
    }
}

Here I’ve also added the componentDidUpdate lifecycle method to the component. This is needed to demonstrate how React adds effects to call this method during the commitphase.

In this article I want to show you how React processes state updates and builds the effects list. We’ll take a tour into what’s going on in the high-level functions for the render and commit phases.

Particularly, we’ll see how that in <a href="https://github.com/facebook/react/blob/cbbc2b6c4d0d8519145560bd8183ecde55168b12/packages/react-reconciler/src/ReactFiberCompleteWork.js#L532" target="_blank">completeWork</a> React:

  • updates the count property in the state of ClickCounter
  • calls the render method to get a list of children and performs comparison
  • updates the props for the span element

And, in <a href="https://github.com/facebook/react/blob/95a313ec0b957f71798a69d8e83408f40e76765b/packages/react-reconciler/src/ReactFiberScheduler.js#L523" target="_blank">commitRoot</a> React:

  • updates the textContent property of the span element
  • calls the componentDidUpdate lifecycle method

But before that, let’s quickly take a look at how the work is scheduled when we call setState in a click handler.

Note that you don’t need to know any of it to use React. This article is about how React works internally.

Scheduling updates#

When we click on the button, the click event is triggered and React executes the callback that we pass in the button props. In our application it simply increments the counter and updates the state:

class ClickCounter extends React.Component {
    ...
    handleClick() {
        this.setState((state) => {
            return {count: state.count + 1};
        });
    }
}   

Every React component has an associated updater which acts as a bridge between the components and the React core. This allows setState to be implemented differently by ReactDOM, React Native, server side rendering, and testing utilities.

In this article we’ll be looking at the implementation of the updater object in ReactDOM, which uses the Fiber reconciler. For the ClickCounter component it’s a <a href="https://github.com/facebook/react/blob/6938dcaacbffb901df27782b7821836961a5b68d/packages/react-reconciler/src/ReactFiberClassComponent.js#L186" target="_blank">classComponentUpdater</a>. It’s responsible for retrieving an instance of Fiber, queuing updates, and scheduling the work.

When updates are queued, they are basically just added to the queue of updates to process on a Fiber node. In our case, the Fiber node corresponding to the ClickCountercomponent will have the following structure:

{
    stateNode: new ClickCounter,
    type: ClickCounter,
    updateQueue: {
         baseState: {count: 0}
         firstUpdate: {
             next: {
                 payload: (state) => { return {count: state.count + 1} }
             }
         },
         ...
     },
     ...
}

As you can see, the function in the updateQueue.firstUpdate.next.payload is the callback we passed to setState in the ClickCounter component.It represents the first update that needs to be processed during the render phase.

Processing updates for the ClickCounter Fiber node#

The chapter on the work loop in my previous article explains the role of the nextUnitOfWork global variable. Particularly, it states that this variable holds a reference to the Fiber node from the workInProgress tree that has some work to do. As React traverses the tree of Fibers, it uses this variable to know if there’s any other Fiber node with unfinished work.

Let’s start with the assumption that the setState method has been called. React adds the callback from <strong>setState</strong> to the updateQueue on the ClickCounter fiber node and and schedules work. React enters the render phase. It starts traversing from the topmost HostRoot Fiber node using the renderRoot function. However, it bails out of (skips) the already processed Fiber nodes until it finds a node with unfinished work. At this point there’s only one Fiber node with some work to do. It’s the ClickCounter Fiber node.

All work is performed on the cloned copy of this Fiber node is stored in the alternatefield. If the alternate node is not yet created, React creates the copy in the function createWorkInProgress before processing updates. Let’s assume that the variable nextUnitOfWork holds a reference to the alternate ClickCounter Fiber node.

beginWork#

First, our Fiber gets into the beginWork function.

The beginWork function is basically a big switch statement that determines the type of work that needs to be done for a Fiber node by the tag and then executes the respective function to perform the work. In the case of CountClicks it’s a class component, so this branch will be taken:

function beginWork(current$1, workInProgress, ...) {
    ...
    switch (workInProgress.tag) {
        ...
        case FunctionalComponent: {...}
        case ClassComponent:
        {
            ...
            return updateClassComponent(current$1, workInProgress, ...);
        }
        case HostComponent: {...}
        case ...
}

and we get into the <a href="https://github.com/facebook/react/blob/1034e26fe5e42ba07492a736da7bdf5bf2108bc6/packages/react-reconciler/src/ReactFiberBeginWork.js#L428" target="_blank">updateClassComponent</a> function. Depending on whether it’s the first rendering of a component, work being resumed, or a React update, React either creates an instance and mounts the component or just updates it:

function updateClassComponent(current, workInProgress, Component, ...) {
    ...
    const instance = workInProgress.stateNode;
    let shouldUpdate;
    if (instance === null) {
        ...
        // In the initial pass we might need to construct the instance.
        constructClassInstance(workInProgress, Component, ...);
        mountClassInstance(workInProgress, Component, ...);
        shouldUpdate = true;
    } else if (current === null) {
        // In a resume, we'll already have an instance we can reuse.
        shouldUpdate = resumeMountClassInstance(workInProgress, Component, ...);
    } else {
        shouldUpdate = updateClassInstance(current, workInProgress, ...);
    }
    return finishClassComponent(current, workInProgress, Component, shouldUpdate, ...);
}

Processing updates for the ClickCounter Fiber#

We already have an instance of the ClickCounter component, so we get into the <a href="https://github.com/facebook/react/blob/6938dcaacbffb901df27782b7821836961a5b68d/packages/react-reconciler/src/ReactFiberClassComponent.js#L976" target="_blank">updateClassInstance</a>. That’s where React performs most of the work for class components. Here are the most important operations performed in the function in the order of execution:

  • call UNSAFE_componentWillReceiveProps<strong>()</strong> hook (deprecated)
  • process updates in the updateQueue and generate new state
  • call getDerivedStateFromProps with this new state and get the result
  • call the shouldComponentUpdate to ensure a component wants to update;
  • if false, skip the whole rendering process, including calling render on this component and its children; otherwise proceed with the update
  • call UNSAFE_componentWillUpdate(deprecated)
  • add an effect to trigger componentDidUpdate** **lifecycle hook
  • update state and props on the component instance

<em>state</em>* and <em>props</em> should be updated on the component instance before the *<em>render</em>method is called, since the <em>render</em> method output usually depends on the <em>state</em> and <em>props</em>. If we don’t do that, it will be returning the same output every time.

Here’s the simplified version of the function:

function updateClassInstance(current, workInProgress, ctor, newProps, ...) {
    const instance = workInProgress.stateNode;

    const oldProps = workInProgress.memoizedProps;
    instance.props = oldProps;
    if (oldProps !== newProps) {
        callComponentWillReceiveProps(workInProgress, instance, newProps, ...);
    }

    let updateQueue = workInProgress.updateQueue;
    if (updateQueue !== null) {
        processUpdateQueue(workInProgress, updateQueue, ...);
        newState = workInProgress.memoizedState;
    }

    applyDerivedStateFromProps(workInProgress, ...);
    newState = workInProgress.memoizedState;

    const shouldUpdate = checkShouldComponentUpdate(workInProgress, ctor, ...);
    if (shouldUpdate) {
        instance.componentWillUpdate(newProps, newState, nextContext);
        workInProgress.effectTag |= Update;
        workInProgress.effectTag |= Snapshot;
    }

    instance.props = newProps;
    instance.state = newState;

    return shouldUpdate;
}

I’ve removed some auxiliary code in the snippet above. For instance, before calling lifecycle methods or adding effects to trigger them, React checks if a component implements the method using the typeof operator. Here is, for example, how React checks for the componentDidUpdate method before adding the effect:

if (typeof instance.componentDidUpdate === 'function') {
    workInProgress.effectTag |= Update;
}

Okay, so now we know what operations are performed for the ClickCounter Fiber node during the render phase. Let’s now see how these operations change values on the Fiber nodes.When React begins work, the Fiber node for the ClickCounter component looks like this:

{
    effectTag: 0,
    elementType: class ClickCounter,
    firstEffect: null,
    memoizedState: {count: 0},
    type: class ClickCounter,
    stateNode: {
        state: {count: 0}
    },
    updateQueue: {
        baseState: {count: 0},
        firstUpdate: {
            next: {
                payload: (state, props) => {…}
            }
        },
        ...
    }
}

After the work is completed, we end up with a Fiber node that looks like this:

{
    effectTag: 4,
    elementType: class ClickCounter,
    firstEffect: null,
    memoizedState: {count: 1},
    type: class ClickCounter,
    stateNode: {
        state: {count: 1}
    },
    updateQueue: {
        baseState: {count: 1},
        firstUpdate: null,
        ...
    }
}

Take a moment to observe the differences in properties values.

After the update is applied, the value of the property count is changed to 1** **in the memoizedState and the baseState in updateQueue. React has also updated the state in the ClickCounter component instance.

At this point, we no longer have updates in the queue, so firstUpdate is null.And importantly, we have changes in the effectTag property. It’s no longer 0, it’s value is <strong>4</strong>. In binary this is 100, which means that the third bit is set, which is exactly the bit for the Update side-effect tag:

export const Update = 0b00000000100;

So to conclude, when working on the parent ClickCounter Fiber node, React calls the pre-mutation lifecycle methods, updates the state and defines relevant side-effects.

Reconciling children for the ClickCounter Fiber#

Once that’s done, React gets into the finishClassComponent. This is where React calls the render method on a component instance and applies its diffing algorithm to the children returned by the component. The high-level overview is described in the docs. Here’s the relevant part:

When comparing two React DOM elements of the same type, React looks at the attributes of both, keeps the same underlying DOM node, and only updates the changed attributes.
If we dig deeper, however, we can learn that it actually compares Fiber nodes with React elements. But I won’t go into much details now as the process is quite elaborate. I’ll write a separate piece that focuses particular on the process of child reconciliation.
If you’re anxious to learn details on your own, check out the reconcileChildrenArrayfunction since in our application the render method returns an array of React Elements.
At this point there are two things that are important to understand. First, as React goes through the child reconciliation process, it creates or updates Fiber nodes for the child React elements returned from the render method. The finishClassComponentfunction returns the reference to the first child of the current Fiber node. It will be assigned to the nextUnitOfWork and processed later in the work loop. Second, React updates the props on the children as part of work performed for the parent. To do that it uses data from the React elements returned from render method.

For example, here’s what the Fiber node corresponding to the span element looks like before React reconciles the children for the ClickCounter fiber:

{
    stateNode: new HTMLSpanElement,
    type: "span",
    key: "2",
    memoizedProps: {children: 0},
    pendingProps: {children: 0},
    ...
}

As you can see, the children property in both memoizedProps and pendingProps is 0. Here’s the structure of the React element returned from the render for the spanelement:

{
    $typeof: Symbol(react.element)
    key: "2"
    props: {children: 1}
    ref: null
    type: "span"
}

As you can see, there’s a difference between the props in the Fiber node and the returned React element. Inside the <a href="https://github.com/facebook/react/blob/769b1f270e1251d9dbdce0fcbd9e92e502d059b8/packages/react-reconciler/src/ReactFiber.js#L326" target="_blank"><strong>createWorkInProgress</strong></a> function that is used to create alternate Fiber nodes, React will copy the updated properties from the React element to the Fiber node.

So, after React has finished reconciling the children for the ClickCounter component, the span Fiber node will have the pendingProps updated. They will match the value in the span React element:

{
    stateNode: new HTMLSpanElement,
    type: "span",
    key: "2",
    memoizedProps: {children: 0},
    pendingProps: {children: 1},
    ...
}

Later, when React will be performing work for the span Fiber node, it will copy them to the memoizedProps and add effects to update DOM.

Well, that’s all the work that React performs for the ClickCounter fiber node during the render phase. Since the button is the first child of the ClickCounter component, it will be assigned to the nextUnitOfWork** **variable. There’s nothing to be done with it, so React will move to its sibling, which is span Fiber node. According to the algorithm described here, it happens in the completeUnitOfWork function.

Processing updates for the Span fiber#

So, the variable nextUnitOfWork now points to the alternate of the span fiber and React starts working on it. Similar to the steps performed for the ClickCounter, we start with the beginWork function.

Since our span node is of HostComponent type, this time in the switch statement React takes this branch:

function beginWork(current$1, workInProgress, ...) {
    ...
    switch (workInProgress.tag) {
        case FunctionalComponent: {...}
        case ClassComponent: {...}
        case HostComponent:
          return updateHostComponent(current, workInProgress, ...);
        case ...
}

and ends up in the <a href="https://github.com/facebook/react/blob/cbbc2b6c4d0d8519145560bd8183ecde55168b12/packages/react-reconciler/src/ReactFiberBeginWork.js#L686" target="_blank">updateHostComponent</a> function. You can see a parallel with the updateClassComponent function called for class components. For a functional component it’ll be updateFunctionComponent and so on. You can find all these functions in the <a href="https://github.com/facebook/react/blob/1034e26fe5e42ba07492a736da7bdf5bf2108bc6/packages/react-reconciler/src/ReactFiberBeginWork.js" target="_blank">ReactFiberBeginWork.js</a> file.

Reconciling children for the span fiber#

In our case there nothing important happening for the span node in the updateHostComponent.

Completing work for the Span Fiber node#

Once beginWork is finished, the node gets into the completeWork function. But before that, React needs to update the memoizedProps on the span fiber. You may remember that when reconciling children for the ClickCounter** **component, React updated the pendingProps on the span Fiber node:

{
    stateNode: new HTMLSpanElement,
    type: "span",
    key: "2",
    memoizedProps: {children: 0},
    pendingProps: {children: 1},
    ...
}

So once beginWork is finished for the <strong>span</strong> fiber, React updates pendingProps to match memoizedProps:

function performUnitOfWork(workInProgress) {
    ...
    next = beginWork(current$1, workInProgress, nextRenderExpirationTime);
    workInProgress.memoizedProps = workInProgress.pendingProps;
    ...
}

It then calls the completeWork function which is basically a big switch statement similar to the one we saw in beginWork:

function completeWork(current, workInProgress, ...) {
    ...
    switch (workInProgress.tag) {
        case FunctionComponent: {...}
        case ClassComponent: {...}
        case HostComponent: {
            ...
            updateHostComponent(current, workInProgress, ...);
        }
        case ...
    }
}

Since our span Fiber node is HostComponent, it runs the <a href="https://github.com/facebook/react/blob/cbbc2b6c4d0d8519145560bd8183ecde55168b12/packages/react-reconciler/src/ReactFiberBeginWork.js#L686" target="_blank">updateHostComponent</a> function. In this function React basically does the following:

  • prepares the DOM updates
  • adds them to updateQueue of the span fiber
  • adds the effect to update the DOM

Before these operations are performed, the span Fiber node looks like this:

{
    stateNode: new HTMLSpanElement,
    type: "span",
    effectTag: 0
    updateQueue: null
    ...
}

and when the work is completed it looks like this:

{
    stateNode: new HTMLSpanElement,
    type: "span",
    effectTag: 4,
    updateQueue: ["children", "1"],
    ...
}

Notice the difference in the effectTag and updateQueue fields. It’s no longer 0, it’s value is 4. In binary this is 100, which means that the third bit is set,which is exactly the bit for the Update side-effect tag. That’s the only job React needs to do for this node during the following commit phase. The updateQueue field holds the payload that will be used for the update.

Once React has processed ClickCounter and its children, it’s done with the renderphase. It can now assign the completed alternate tree to the finishedWork property on FiberRoot. This is the new tree that needs to be flushed to the screen. It can be processed immediately after the render phase or picked up later when React is given time by the browser.

Effects list#

In our case, since the span node and the ClickCounter component have side effects, React will add a link to the span Fiber node to the firstEffect property of HostFiber.

React builds the effects list in the <a href="https://github.com/facebook/react/blob/d5e1bf07d086e4fc1998653331adecddcd0f5274/packages/react-reconciler/src/ReactFiberScheduler.js#L999" target="_blank">compliteUnitOfWork</a> function. Here’s what a Fiber tree with effects to update text of the span node and calls hooks on ClickCounter looks like:

And here’s the linear list of nodes with effects:

Commit phase#

This phase begins with the function completeRoot. Before it gets to do any work, it sets the finishedWork property on the FiberRoot to null:

root.finishedWork = null;

Unlike the first render phase, the commit phase is always synchronous so it can safely update HostRoot to indicate that the commit work has started.

The commit phase is where React updates the DOM and calls the post mutation lifecycle method componentDidUpdate. To do that, it goes over the list of effects it constructed during the previous render phase and applies them.

We have the following effects defined in the render phase for our span and ClickCounter nodes:

{ type: ClickCounter, effectTag: 5 }
{ type: 'span', effectTag: 4 }

The value of the effect tag for ClickCounter** is 5 or 101 in binary and defines the Update **work which basically translates into the componentDidUpdate lifecycle method for class components. The least significant bit is also set to signal that all work has been completed for this Fiber node in the render phase.

The value of the effect tag for span is 4 or 100 in binary and defines the update** work for the host component DOM update. In the case of the span element, React will need to update textContent **for the element.

Applying effects#

Let’s see how React applies those effects. The function <a href="https://github.com/facebook/react/blob/95a313ec0b957f71798a69d8e83408f40e76765b/packages/react-reconciler/src/ReactFiberScheduler.js#L523" target="_blank">commitRoot</a>, which is used to apply the effects, consists of 3 sub-functions:

function commitRoot(root, finishedWork) {
    commitBeforeMutationLifecycles()
    commitAllHostEffects();
    root.current = finishedWork;
    commitAllLifeCycles();
}

Each of those sub-functions implements a loop that iterates over the list of effects and checks the types of the effects. When it finds the effect pertaining to the function’s purpose, it applies it. In our case, it will call the componentDidUpdate lifecycle method on the ClickCounter component and update the text of the span element.

The first function commitBeforeMutationLifeCycles looks for the <a href="https://github.com/facebook/react/blob/b87aabdfe1b7461e7331abb3601d9e6bb27544bc/packages/shared/ReactSideEffectTags.js#L25" target="_blank">Snapshot</a> effect and calls the getSnapshotBeforeUpdate method. But, since we didn’t implement the method on the ClickCounter component, React didn’t add the effect during the render stage. So in our case, this function does nothing.

DOM updates#

Next React moves to the <a href="https://github.com/facebook/react/blob/95a313ec0b957f71798a69d8e83408f40e76765b/packages/react-reconciler/src/ReactFiberScheduler.js#L376" target="_blank">commitAllHostEffects</a> function. This is where React will change the text on the span element from 0 to 1. There’s nothing to do for the ClickCounter fiber because nodes corresponding to class components don’t have any DOM updates.

The gist of the function is that it selects the correct type of effect and applies the corresponding operations. In our case we need to update the text on the span element, so we take the Update branch here:

function updateHostEffects() {
    switch (primaryEffectTag) {
      case Placement: {...}
      case PlacementAndUpdate: {...}
      case Update:
        {
          var current = nextEffect.alternate;
          commitWork(current, nextEffect);
          break;
        }
      case Deletion: {...}
    }
}

By going down to commitWork, we will eventually get into the <a href="https://github.com/facebook/react/blob/8a8d973d3cc5623676a84f87af66ef9259c3937c/packages/react-dom/src/client/ReactDOMComponent.js#L326" target="_blank">updateDOMProperties</a>function. It takes the updateQueue payload that was added during the render stage to the Fiber node, and updates the textContent property on the span element:

function updateDOMProperties(domElement, updatePayload, ...) {
  for (let i = 0; i < updatePayload.length; i += 2) {
    const propKey = updatePayload[i];
    const propValue = updatePayload[i + 1];
    if (propKey === STYLE) { ...} 
    else if (propKey === DANGEROUSLY_SET_INNER_HTML) {...} 
    else if (propKey === CHILDREN) {
      setTextContent(domElement, propValue);
    } else {...}
  }
}

After the DOM updates have been applied, React assigns the finishedWork tree to the HostRoot. It sets an alternate tree as current:

root.current = finishedWork;

Calling post mutation lifecycle hooks#

The last remaining function is <a href="https://github.com/facebook/react/blob/d5e1bf07d086e4fc1998653331adecddcd0f5274/packages/react-reconciler/src/ReactFiberScheduler.js#L479" target="_blank"><strong>commitAllLifecycles</strong></a>. This where React calls the post mutational lifecycle methods. During the render phase, React added the Update effect to the ClickCounter component. This is one of the effects that the function commitAllLifecycles looks for and calls componentDidUpdate method:

function commitAllLifeCycles(finishedRoot, ...) {
    while (nextEffect !== null) {
        const effectTag = nextEffect.effectTag;

        if (effectTag & (Update | Callback)) {
            const current = nextEffect.alternate;
            commitLifeCycles(finishedRoot, current, nextEffect, ...);
        }
        
        if (effectTag & Ref) {
            commitAttachRef(nextEffect);
        }
        
        nextEffect = nextEffect.nextEffect;
    }
}

The function also updates refs, but since we don’t have any this functionality won’t be used. The method is called in the <a href="https://github.com/facebook/react/blob/e58ecda9a2381735f2c326ee99a1ffa6486321ab/packages/react-reconciler/src/ReactFiberCommitWork.js#L351" target="_blank">commitLifeCycles</a> function:

function commitLifeCycles(finishedRoot, current, ...) {
  ...
  switch (finishedWork.tag) {
    case FunctionComponent: {...}
    case ClassComponent: {
      const instance = finishedWork.stateNode;
      if (finishedWork.effectTag & Update) {
        if (current === null) {
          instance.componentDidMount();
        } else {
          ...
          instance.componentDidUpdate(prevProps, prevState, ...);
        }
      }
    }
    case HostComponent: {...}
    case ...
}

You can also see that this is the function where React calls the componentDidMountmethod for components that have been rendered for the first time.

And that’s it!

We’re finally done. Let me know what you think about the article or ask questions in the comments. I have many more articles in the works providing in-depth explanation for scheduler, children reconciliation process and how effects list is built. I also have plans to create a video where I’ll show how to debug the application using this article as a basis.

Recommended Reading

React Native and Apollo GraphQL Tutorial: Build Mobile Apps

The React Cookbook: Advanced Recipes to Level Up Your Next App

Firebase login functionality from scratch with React + Redux

Breaking Down the Last Monolith

Have Confidence in Your Redux Apps With Integration and Unit Testing

Setup Continuous Integration (CI) With React, CircleCI, and GitHub

React Redux binding with React Hooks and Proxy

#reactjs #javascript

Understanding State and Props in ReactJS
15.75 GEEK