In-depth Understanding of React: The Complete Workflow of completeWork

Time: Column:Mobile & Frontend views:240

Every Fiber node that goes through beginWork will eventually reach completeWork. When beginWork has progressed to the point where it cannot go any deeper—in other words, when the current node has no child nodes—it will enter completeWork, as shown below:

function performUnitOfWork(unitOfWork) { // The current workInProgress node
    next = beginWork(current, unitOfWork, subtreeRenderLanes);
    if (next === null) { // When there are no child nodes, we’ve reached the end
        completeUnitOfWork(unitOfWork);
    } else {
        workInProgress = next;
    }
    ReactCurrentOwner$2.current = null;
}

completeUnitOfWork Traverses Sibling and Parent Nodes

completeUnitOfWork first checks for sibling nodes:

  • If there is a sibling, it moves to that sibling’s beginWork in the next loop cycle.

  • If there is no sibling, it moves up to the parent node’s completeWork in the current loop.

function completeUnitOfWork(unitOfWork) {
    var completedWork = unitOfWork; // This is the fiber node without children
    do {
        var current = completedWork.alternate;
        var returnFiber = completedWork.return;
        
        var next = completeWork(current, completedWork, subtreeRenderLanes); 

        if (next !== null) { // Normally, `next` returns null
            workInProgress = next;
            return;
        }
        
        var siblingFiber = completedWork.sibling;
        if (siblingFiber !== null) { // If there’s a sibling, enter its `beginWork`
            workInProgress = siblingFiber;
            return;
        }
        
        // If no sibling, move up to the parent’s `completeWork`
        completedWork = returnFiber; 
        workInProgress = completedWork;
    } while (completedWork !== null); // Until we reach the root
}

What Does completeWork Do?

The completeWork function handles different scenarios based on the fiber type:

function completeWork(current, workInProgress, renderLanes) {
    var newProps = workInProgress.pendingProps;
    // Enter different logic branches based on the type
    switch(workInProgress.tag) {
        case IndeterminateComponent:
        case FunctionComponent:
            ...
            bubbleProperties(workInProgress);
            return null;

        case ClassComponent: {
            ...
            bubbleProperties(workInProgress);
            return null;
        }
        
        case HostRoot: {
            var fiberRoot = workInProgress.stateNode;
            popHostContainer(workInProgress);
            updateHostContainer(current, workInProgress);
            bubbleProperties(workInProgress);
            return null;
        }
        ...
    }
}

completeWork processes different branches based on the fiber type, handling distinct cases. Let’s examine two scenarios: initialization and updates.

Initialization

HostComponent

The first node to enter completeWork is typically a Fiber with no children, usually a HostComponent Fiber type. When it enters completeWork, it triggers the following logic:

// Creates an instance node, which is a real DOM object for HostComponent
var instance = createInstance(
    type, // div
    newProps, // { onClick, children, id, className ... }
    rootContainerInstance, // #root
    currentHostContext, // Not important here
    workInProgress // Fiber#div
);
// Adds its child DOM nodes to its children list, touching the real DOM
appendAllChildren(instance, workInProgress, false, false);
// Adds the instance to the Fiber’s stateNode
workInProgress.stateNode = instance;
// Handles properties
bubbleProperties(workInProgress);

appendAllChildren iterates through the fiber node’s children and adds their stateNode properties to instance, resulting in a fully constructed off-screen DOM tree by the time it reaches the root node. This bottom-up approach allows completeWork to merge from the leaf nodes into a complete DOM tree in memory.

Next, bubbleProperties collects each type of fiber flag, which is crucial for the subsequent commit phase:

function bubbleProperties(completedWork) {
    var didBailout = completedWork.alternate !== null && completedWork.alternate.child === completedWork.child;
    var newChildLanes = NoLanes;
    var subtreeFlags = NoFlags;

    if (!didBailout) { // Initialization
        var _child = completedWork.child;
        // Collect flags and lanes from direct children, assigning them to `childLanes` and `subtreeFlags` for the workInProgress
        while (_child !== null) {
            newChildLanes = mergeLanes(newChildLanes, mergeLanes(_child.lanes, _child.childLanes));
            subtreeFlags |= _child.subtreeFlags;
            subtreeFlags |= _child.flags; 
            _child.return = completedWork;
            _child = _child.sibling;
        }
        completedWork.subtreeFlags |= subtreeFlags; // Merge subtree effects
    } else { // Update
        ...
    }
    completedWork.childLanes = newChildLanes;
    return didBailout;
}

Each fiber node gathers all tags from its child nodes. These tags represent the operations to be performed on the actual DOM in the commit phase. Here are some flag types representing various effects in React:

export const NoFlags = /*                      */ 0b00000000000000000000000000; // No side effects
export const PerformedWork = /*                */ 0b00000000000000000000000001; // Related to devTools

export const Placement = /*                    */ 0b00000000000000000000000010; // Insert
export const Update = /*                       */ 0b00000000000000000000000100; // Update
export const Deletion = /*                     */ 0b00000000000000000000001000; // Delete
export const ChildDeletion = /*                */ 0b00000000000000000000010000; // Delete child node
export const ContentReset = /*                 */ 0b00000000000000000000100000; // Reset content
export const Callback = /*                     */ 0b00000000000000000001000000; // Callback function
// And many more…

export const LifecycleEffectMask = Passive | Update | Callback | Ref | Snapshot | StoreConsistency; // Lifecycle side effects

In both beginWork and completeWork, side effect flags are marked on the current fiber node to dictate the specific DOM or side effect operations during the commit phase.

HostRoot

During initialization, when entering HostRoot, the following process occurs:

// Simplified example
workInProgress.flags |= Snapshot;
bubbleProperties(workInProgress);

The HostRoot receives a Snapshot flag, which will clear the previous UI during the commit phase by setting root.textContent = ''. Once completeWork reaches HostRoot, it signals the end of the initialization render process, after which updates can begin.

Update Phase

During the update phase, the first node encountered is the HostComponent, following this logic:

if (current !== null && workInProgress.stateNode != null) { // During updates
  updateHostComponent$1( // If there’s an update, tag it
    current,
    workInProgress,
    type,
    newProps,
    rootContainerInstance
  );

  if (current.ref !== workInProgress.ref) {
    markRef$1(workInProgress); // Tag it
  }
} else {
  // Initialization...
}
bubbleProperties(workInProgress);

For HostComponent, the main focus during the update phase is not to create DOM nodes but to detect any updates. Within updateHostComponent$1, the diffProperties() function checks for differences between the new and old props. If differences are found, an Update tag is applied.

updateHostComponent$1 = function (
  current,
  workInProgress,
  type,
  newProps, // New props
  rootContainerInstance // Current DOM object
) {
  var oldProps = current.memoizedProps;
  if (oldProps === newProps) { // No tag if props are identical
    return;
  } 
  var instance = workInProgress.stateNode;
  var currentHostContext = getHostContext(); 
  var updatePayload = prepareUpdate( // Check for differences
    instance,
    type,
    oldProps,
    newProps,
    rootContainerInstance,
    currentHostContext
  ); 
  workInProgress.updateQueue = updatePayload; 
  if (updatePayload) { // Apply update tag
    markUpdate(workInProgress);
  }
};

This exemplifies the main behavior for HostComponent during the update phase. The bubbleProperties function in this phase continues to collect flags from its subtree.

Summary:

To conclude, here are the main responsibilities of completeWork:

  1. Create DOM nodes

  2. Apply tags

  3. Collect child tags

  4. Build an offscreen DOM tree

Once completeWork completes the last Fiber node, it marks the end of the render phase. At the end of completeUnitOfWork, workInProgressRootExitStatus is set to RootCompleted, signaling that the Fiber tree construction for this render phase is complete.

function completeUnitOfWork(unitOfWork){
  var completedWork = unitOfWork;
  
  do {
    ...
  } while (completedWork !== null);
  
  if (workInProgressRootExitStatus === RootInProgress) {
    workInProgressRootExitStatus = RootCompleted;
  }
}

Finally, in renderRootSync or renderRootConcurrent, the workInProgressRootExitStatus is returned to the parent layer, which then determines the next actions based on the constructed Fiber tree’s state. If the status is RootCompleted, the workInProgress tree is assigned to root.finishedWork, leading into the commit phase.

function performSyncWorkOnRoot(root) {
  ...
  var exitStatus = renderRootSync(root, lanes); // Render phase

  if (exitStatus === RootFatalErrored) { // Incomplete case
    ...
    throw fatalError;
  }

  if (exitStatus === RootDidNotComplete) { // Incomplete case
    throw new Error("Root did not complete. This is a bug in React.");
  }
  var finishedWork = root.current.alternate; // The `workInProgress` tree
  root.finishedWork = finishedWork;
  root.finishedLanes = lanes;
  // Enter commit phase
  commitRoot(
    root,
    workInProgressRootRecoverableErrors,
    workInProgressTransitions
  ); 
  // Schedule the next update
  ensureRootIsScheduled(root, now());
  return null;
}