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 ClassComponent
s 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.
getChildContext()
is marked as legacy and may not be active or bundled anymore.
Avoid this API.
How completing HostRoot
works
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.