Skip to main content

How completeWork works

A few sections back, we rendered our application using React. This process was very similar to the following code:

// simplified
function workLoopSync() {
// Perform work without checking if we need to yield between fiber.
while (workInProgress !== null) {
performUnitOfWork(workInProgress);
}
}

function performUnitOfWork(unitOfWork: Fiber): void {
const current = unitOfWork.alternate;

const next = beginWork(current, unitOfWork, renderLanes);

unitOfWork.memoizedProps = unitOfWork.pendingProps;
if (next === null) {
// If this doesn't spawn new work, complete the current work.
completeUnitOfWork(unitOfWork);
} else {
workInProgress = next;
}
}

Like we mentioned in an earlier section, React work loop will render a single path from the fiberRoot passing direct first child by direct first child until it reaches the very bottom.

In this case, beginWork will return a null fiber, which means there is no more work to do in the current tree.

That's the role of the completeUnitOfWork function: it will infer the next component to render.

We will see in details how the next fiber is chosen.

Signature

function completeUnitOfWork(unitOfWork: Fiber): void {

}

completeUnitOfWork is called with the workInProgress variable that refers to the completed work of a component.

Implementation

The goal of this function is to decide the next fiber to work on.

Here is a simplified version of it:

// simplified
let completeWork = unitOfWork;

do {
const current = completeWork.alternate;
const returnFiber = completedWork.return;
// complete work is a huge function that contains the real implementation
// we will see it in a few.
const next = completeWork(current, completeWork, renderLanes);

// complete work decided that there is still some work to do
if (next !== null) {
workInProgress = next;
return;
}

// if there is no next, take the sibling
const siblingFiber = completeWork.sibling;
if (siblingFiber !== null) {
workInProgress = siblingFiber;
return;
}

// otherwise, take the returnFiber of the completeWork
completedWork = returnFiber;
workInProgress = completedWork;
} while (completeWork !== null);
//...

So completeUnitOfWork will keep climbing back the tree while passing by the siblings and so on.

On important thing to mention is that while passing to a sibling, performUnitOfWork will pass through all its children until it reaches the bottom again, then climb back using completeUnitOfWork.

But, completeUnitOfWork doesn't do all the work, it calls a huge function of around 1000 lines of code (completeWork) that we will see right now.

It is important to check this function for two reasons: The first is that it is called from within completeUnitOfWork to decide the next fiber, and as fallback when null is returned, it will look up on the sibling and parent.

How completeWork works

completeWork is a huge function with a big switch statement.

Signature

function completeWork(
current: Fiber | null,
workInProgress: Fiber,
renderLanes: Lanes,
): Fiber | null {
switch (workInProgress.tag) {
case IndeterminateComponent:
case LazyComponent:
case SimpleMemoComponent:
case FunctionComponent:
case ForwardRef:
case Fragment:
case Mode:
case Profiler:
// .... other tags
{

}
}
}

completeWork will perform the work based on the tag of the finished work.

How completing FunctionComponent works

Function component is treated like any of these: IndeterminateComponent, LazyComponent,SimpleMemoComponent,FunctionComponent,ForwardRef, Fragment,Mode,Profiler,ContextConsumer,MemoComponent.

And it does the following:

switch (workInProgress.tag) {
case IndeterminateComponent:
case LazyComponent:
case SimpleMemoComponent:
case FunctionComponent:
case ForwardRef:
case Fragment:
case Mode:
case Profiler:
case ContextConsumer:
case MemoComponent:
bubbleProperties(workInProgress);
return null;
}

bubbleProperties is invoked on completed work, and it serves to merge the flags of the children inside their parent (the complete work):

// simplified
function bubbleProperties(completedWork: Fiber) {
let subtreeFlags = NoFlags;
let newChildLanes = NoLanes;
let child = completedWork.child;

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;
completedWork.childLanes = newChildLanes;
}

How completing ClassComponent works

Finishing the work for ClassComponents is similar to FunctionComponent and the others since it will bubble the properties as well (this will be done in almost all cases).

switch (workInProgress.tag) {
// ...
case ClassComponent: {
const Component = workInProgress.type;
if (isLegacyContextProvider(Component)) {
popLegacyContext(workInProgress);
}
// same as FunctionComponent
bubbleProperties(workInProgress);
return null;
}
// ...
}

ClassComponents have this special branching where they can be context providers at the same time using the legacy getChildContext() API.

danger

getChildContext() is marked as legacy and may not be active or bundled anymore. Avoid this API.

How completing HostRoot works

Switch case

This section is not available yet. Please fill an issue.

While waiting for the newsletter, you can get notified when new content drops by following me on X.

How other tags work

This section is not available yet. Please fill an issue.

While waiting for the newsletter, you can get notified when new content drops by following me on X.

Recap

When all work is completed, and we rendered until reaching the very bottom of each node of our tree, the performConcurrentWorkOnRoot will proceed then to completing the render and committing the root.

Please take a look at the following code, we've already seen it, but it is better that you refresh your memory to prepare for the next section.

// simplified to only include relevant things
function performConcurrentWorkOnRoot(
root: FiberRoot,
didTimeout: boolean,
): RenderTaskFn | null {
// previous code

const exitStatus = shouldTimeSlice
? renderRootConcurrent(root, lanes)
: renderRootSync(root, lanes);

if (exitStatus !== RootInProgress) {
do {
if (exitStatus === RootDidNotComplete) {
markRootSuspended(root, lanes);
} else {
const finishedWork: Fiber = root.current.alternate;

if (
renderWasConcurrent &&
!isRenderConsistentWithExternalStores(finishedWork)
) {
exitStatus = renderRootSync(root, lanes);
continue;
}
if (exitStatus === RootErrored) {
// ...
}
if (exitStatus === RootFatalErrored) {
// ...
throw fatalError;
}

root.finishedWork = finishedWork;
root.finishedLanes = lanes;
finishConcurrentRender(root, exitStatus, finishedWork, lanes);
}
break;
} while (true);
}
}

The whole workLoop occurred inside renderRootSync or renderRootConcurrent depending on your render lanes.

So after all work is done, the following code is what's important at this stage, specifically the finishConcurrentRender.

root.finishedWork = finishedWork;
root.finishedLanes = lanes;
finishConcurrentRender(root, exitStatus, finishedWork, lanes);

Let's dive into finishing the render and the commit phase in the next section.