How beginWork works
Begin works is the function in React that will render your application.
We invoked it in the previous section like this:
// simplified
function performUnitOfWork(unitOfWork: Fiber): void {
const current = unitOfWork.alternate;
// current: the original fiber: what's visible on the screen
// unitOfWork: the alternate fiber: the current ongoing render
// renderLanes: were globally assigned in prepareFreshStack
const next = beginWork(current, unitOfWork, renderLanes);
// ... rest of the code
}
Signature
beingWork
is defined as follows:
function beginWork(
current: Fiber | null, // the current fiber's tree
workInProgress: Fiber, // the alternate
renderLanes: Lanes,
): Fiber | null { /* [Not Native Code] */ }
It returns the next unit of work, we will see how it calculates it. The next unit of work will be the alternate of the next current fiber.
Implementation
React components can be rendered several times in their lifetime, the alternate
is created for each render that occurs as a draft of the next version of the
component's output.
beginWork
's simplified version looks like this:
function beginWork(
current,
wip,
lanes
) {
if (current !== null) {
// this component is updating
} else {
// this component renders for the first time
}
}
Attempt early bailout if applicable
So beginWork
will first check if we deal with a re-render, this is not the
case when coming from root.render()
but we will get into it anyway. Because
this code path is visited at every render.
Even when coming from root.render
, the HostRoot
fiber will have the
alternate
created since we created it when preparing a fresh stack. But not
the rest of the tree.
So, when the alternate already exists:
Reference the
oldProps
(current.memoizedProps
) andnewProps
(alternate(wip).pendingProps
):const oldProps = current.memoizedProps;
const newProps = workInProgress.pendingProps;If the
oldProps
andnewProps
aren't the same (meaning a triggered render from the parent component) or the legacy context changed, then it mark the component as did receive update.When props did change, it will check if there is a scheduled update or some context changed:
- The component received an update means that the alternate
lanes
intersect with therenderLanes
(do a pause a think about it). - Contexts are stored as
fiber.dependencies
in a linked list, so it will iterate through all of them and compare the context's value.
When nothing changed, React will attempt to bail out the render for this component and its children if possible. We will see the bailout in its dedicated section.
- The component received an update means that the alternate
When the
current
isnull
(first render of a component), then it will mark the component as it did not receive an update, then it will perform some hydration related stuff. This is out of our scope for now.
Let's put the previous words into code:
// simplified
function beginWork(
current: Fiber | null, // the painted fiber if any (garanteed for HostRoot)
workInProgress: Fiber, // the pending rendered fiber
renderLanes: Lanes,
) {
if (current !== null) {
const oldProps = current.memoizedProps;
const newProps = workInProgress.pendingProps;
if (oldProps !== newProps || hasLegacyContextChanged()) {
didReceivedUpdate = true;
} else {
if (
hasScheduledUpdateOrContext(current, renderLanes) &&
// more on this later
(workInProgress.flags & DidCapture === NoFlags)
) {
didReceiveUpdate = false;
return attemptEarlyBailoutIfNoScheduledUpdate(
current,
workInProgress,
renderLanes,
);
}
}
} else {
didReceiveUpdate = false;
// out of our scope code
}
}
As a recap, beginWork
would attempt to bail out the work if not needed.
This is not the only work bailout on React, we will see it several times again.
Render the components
Right next, React will perform a huge switch statement
over workInProgress.tag
.
The goal of this switch is to redirect to the right function that will perform the render on the current fiber.
We will see the workTag
s in details in the next section, so for now, let's
just scratch the surface when coming from root.render()
. Then in the next
section, we will dive into the work tags and how they work before visiting
the render of each one of them.
// simplified
function beginWork(
current: Fiber | null,
workInProgress: Fiber,
renderLanes: Lanes,
) {
// previous code
switch(workInProgress.tag) {
// case FunctionComponent:
// case ClassComponent:
// case IndeterminateComponent:
case HostRoot:
return updateHostRoot(current, workInProgress, renderLanes);
// case HostPortal:
// case HostComponent:
// case HostText:
// case Fragment:
// case Mode:
// case ContextConsumer:
// case ContextProvider:
// case ForwardRef:
// case Profiler:
// case SuspenseComponent:
// case MemoComponent:
// case SimpleMemoComponent:
// case LazyComponent:
// case IncompleteClassComponent:
// case DehydratedFragment:
// case SuspenseListComponent:
// case ScopeComponent:
// case OffscreenComponent:
// case LegacyHiddenComponent:
// case CacheComponent:
// case TracingMarkerComponent:
// case HostHoistable:
// case HostSingleton:
}
throw new Error(
`Unknown unit of work tag (${workInProgress.tag}). This error is likely caused by a bug in ` +
'React. Please file an issue.',
);
}
So as you see, there is a case for every supported tag.
For the very first render while having the HostRoot
fiber, we'll return
updateHostRoot(current, workInProgress, renderLanes)
.
Let's scratch updateHostRoot
too.
PS: This illustrates the first ever render of the root.
function updateHostRoot(
current: null | Fiber,
workInProgress: Fiber,
renderLanes: Lanes,
) {
// we will ignore this for now
pushHostRootContext(workInProgress);
// defensive guard; the root fiber always has the current and alternate
if (current === null) {
throw new Error('Should have a current fiber. This is a bug in React.');
}
const nextProps = workInProgress.pendingProps; // nextProps is null
const prevState = workInProgress.memoizedState; // { element: null }
const prevChildren = prevState.element; // prevChildren = null
cloneUpdateQueue(current, workInProgress);
processUpdateQueue(workInProgress, nextProps, null, renderLanes);
// let's stop here for now, since the next code would assume that
// the updateQueue processing is done and would use properties accordingly
// we will continue the code after explaining clone and process the queue
}
Cloning the updateQueue
The updateQueue
was defined in how createRoot
works and its pending
share queue was populated during how root.render
works.
Now, and assuming we reach this path from the first render and during the
runtime of the app, this function
will attach a new cloned updateQueue
to the alternate
if it has the
same queue
as the current
fiber
.
function cloneUpdateQueue<State>(
current: Fiber,
workInProgress: Fiber,
): void {
// Clone the update queue from current. Unless it's already a clone.
const queue: UpdateQueue<State> = workInProgress.updateQueue;
const currentQueue: UpdateQueue<State> = current.updateQueue;
if (queue === currentQueue) {
const clone: UpdateQueue<State> = {
baseState: currentQueue.baseState,
firstBaseUpdate: currentQueue.firstBaseUpdate,
lastBaseUpdate: currentQueue.lastBaseUpdate,
shared: currentQueue.shared, // shared.pending is what's intersting here
callbacks: null,
};
workInProgress.updateQueue = clone;
}
}
Processing the update queue
This process is long and very complex, I will simplify it a lot and move on because it is not that interesting at this stage, we will see it again.
This cloning and processing paths are achievable from HostRoot
s,
ClassComponent
s and the upcoming experimental CacheComponent
s.
The pending queue being cyclic and has at most two entries, the most recent
as the last. It will be disconnected and then processed in a big while(true)
loop.
This section will be moved into the annex, it is highly complex and would take a lot of time to be explained.
Here is a simplified version that would allow us continuing the render phase:
// coming from root.render(), we have only one update in a cyclic way
prepareTheOrderOfUpdatesToProcess();
let update = firstBaseUpdate;
let newState = queue.baseState;
do {
const queue = workInProgress.updateQueue;
newState = getStateFromUpdate(wip, queue, update, newState, props);
if (update.callback) {
queue.callbacks.push(update.callback);
}
update = update.next;
// no pending update
if (update === null) {
if (queue.shared.pending === null) {
break;
} else {
update = appendPendingUpdates();
}
}
} while (true);
workInProgress.lanes = newLanes;
workInProgress.memoizedState = newState;
The getStateFromUpdate
will switch over the update.tag
(update state for now), which will result in
a state with { element }
containing the children
we gave to root.render()
.
Let's now get back to updateHostRoot
:
function updateHostRoot(
current: null | Fiber,
workInProgress: Fiber,
renderLanes: Lanes,
) {
// we will ignore this for now
pushHostRootContext(workInProgress);
// defensive guard; the root fiber always has the current and alternate
if (current === null) {
throw new Error('Should have a current fiber. This is a bug in React.');
}
const nextProps = workInProgress.pendingProps; // nextProps was null here
const prevState = workInProgress.memoizedState; // { element: null }
const prevChildren = prevState.element; // prevChildren = null
cloneUpdateQueue(current, workInProgress);
processUpdateQueue(workInProgress, nextProps, null, renderLanes);
// we are here now
// nextState = {element: children, isDehydrated: false, cache: {...} }
const nextState: RootState = workInProgress.memoizedState;
const root: FiberRoot = workInProgress.stateNode;
const nextChildren = nextState.element;
if (nextChildren === prevChildren) {
return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
}
reconcileChildren(current, workInProgress, nextChildren, renderLanes);
return workInProgress.child;
}
bailoutOnAlreadyFinishedWork
and reconcileChildren
are key functions during
render and are called from many places, they deserve their own chapter.
During the first render, we won't pass through bailoutOnAlreadyFinishedWork
,
but reconcileChildren
is a key here! So let's scratch it too:
export function reconcileChildren(
current: Fiber | null,
workInProgress: Fiber,
nextChildren: any,
renderLanes: Lanes,
) {
// initial mount of fibers, HostRoot won't pass here
if (current === null) {
workInProgress.child = mountChildFibers(
workInProgress,
null,
nextChildren,
renderLanes,
);
} else {
workInProgress.child = reconcileChildFibers(
workInProgress, // the returnFiber (the parent of the children)
current.child, // the current painted first child
nextChildren, // the new first child
renderLanes, // the lanes used during this render (DefaultLane from root.render)
);
}
}
When processing the HostRoot
fiber, the current always exists and thus
reconcileChildFibers
will call reconcileChildFibers
which resets thenable counter then calls reconcileChildFibersImpl
with the same arguments.
How reconcileChildFibersImpl
works
This function is responsible for rendering the children
, like we saw earlier
the children can be of many forms, such as an array
of elements, string
...
and so on.
function reconcileChildFibersImpl(
returnFiber: Fiber,
currentFirstChild: Fiber | null,
newChild: any,
lanes: Lanes,
): Fiber | null {
// [Not Native Code]
}
Let dive into this function step by step:
Skip over unkeyed top level fragment
First, React will verify if you top level child is a
Fragment
without a key props. If that's the case, then it will skip over thatFragment
// unkeyed top level fragment is know like this:
const isUnkeyedTopLevelFragment =
typeof newChild === 'object' &&
newChild !== null &&
newChild.type === REACT_FRAGMENT_TYPE &&
newChild.key === null;
if (isUnkeyedTopLevelFragment) {
newChild = newChild.props.children;
}When the given children are a non-null object then it will switch over the
$$typeof
property. Dan gave a great blog post [about this property].(https://overreacted.io/why-do-react-elements-have-typeof-property/)if (typeof newChild === 'object' && newChild !== null) {
switch(newChild.$$typeof) {
case REACT_ELEMENT_TYPE: {
return placeSingleChild(
reconcileSingleElement(returnFiber, currentFirstChild, newChild, lanes)
);
}
case REACT_PORTAL_TYPE: {
return placeSingleChild(
reconcileSinglePortal(returnFiber, currentFirstChild, newChild, lanes)
);
}
case REACT_LAZY_TYPE: {
// ignore for now
}
}
}
if (isArray(newChild)) {
// this will be delayed to the future
return reconcileChildrenArray(returnFiber, currentFirstChild, newChild, lanes);
}
if (getIteratorFn(newChild)) {
// this will be delayed to the future
return reconcileChildrenIterator(returnFiber, currentFirstChild, newChild, lanes);
}reconcileChildrenArray
andreconcileChildrenIterator
will be revisited later.If the children are an object when a
.then
function property (async components 😉
), then the promise will be unwrapped and its result is given toreconcileChildFibersImpl
again:if (typeof newChild.then === "function") {
return reconcileChildFibersImpl(
returnFiber,
currentFirstChild,
unwrapThenable(newChild),
lanes
);
}dangerAsync components are experimental and not well-supported in the client since any render will output a new Promise and thus remove the previous tree. Don't use them without a caching strategy (they were originally designed for the server were they are invoked once per request.).
As you may have noticed already, the reconciliation passes before the actual render.
The goal of the reconciliation is to port the next tree to the alternate that we created from the current.
At this stage coming from root.render()
there is not even a Fiber created
for out children, so the first step will be to create it.
how reconcileSingleElement
works
This is actually a part of the reconciliation which has its own section.
The first thing this function does it to verify from the currentFirstChild
if key
or type
changed then it will remove the child by adding it
to a deletions
property in the parentFiber
. This will keep track of
deleted fibers so that we can invoke their cleanup effects in the commit phase.
Next, this function will create and return a new fiber for our application:
const created = createFiberFromElement(element, returnFiber.mode, lanes);
When coming from root.render()
, the first fiber created by this will be the
third of our journey:
- The first is the current fiber attached to the fiber root
- The second is its alternate
- The third (at least) is for the first child we gave to
root.render()
By now, we have two unexplained major sections:
- The big
switch-case
in thebeginWork
function, it will be explained in thehow rendering works
section. - How
reconcileSingleElement
works and how fibers are created, which will be explained in the howthe reconciliation works
section.
To avoid making this section very huge, we will move them next.
Recap
beginWork
is called inside performUnitOfWork(workInProgress)
which will
be responsible for reconciling the children for the next tree before rendering
it.
// simplified in renderSync
while(unitOfWork !== null) {
performUnitOfWork(unitOfWork);
}
function beginWork(
current: Fiber | null,
workInProgress: Fiber,
renderLanes: Lanes,
) {
if (current !== null) {
const oldProps = current.memoizedProps;
const newProps = workInProgress.pendingProps;
if (oldProps !== newProps || hasLegacyContextChanged()) {
didReceivedUpdate = true;
} else {
if (
hasScheduledUpdateOrContext(current, renderLanes) &&
// more on this later
(workInProgress.flags & DidCapture === NoFlags)
) {
didReceiveUpdate = false;
return attemptEarlyBailoutIfNoScheduledUpdate(
current,
workInProgress,
renderLanes,
);
}
}
} else {
didReceiveUpdate = false;
// out of our scope code
}
switch(workInProgress.tag) {
// case FunctionComponent:
// case ClassComponent:
// case IndeterminateComponent:
case HostRoot:
return updateHostRoot(current, workInProgress, renderLanes);
// case HostPortal:
// case HostComponent:
// case HostText:
// case Fragment:
// case Mode:
// case ContextConsumer:
// case ContextProvider:
// case ForwardRef:
// case Profiler:
// case SuspenseComponent:
// case MemoComponent:
// case SimpleMemoComponent:
// case LazyComponent:
// case IncompleteClassComponent:
// case DehydratedFragment:
// case SuspenseListComponent:
// case ScopeComponent:
// case OffscreenComponent:
// case LegacyHiddenComponent:
// case CacheComponent:
// case TracingMarkerComponent:
// case HostHoistable:
// case HostSingleton:
}
throw new Error(
`Unknown unit of work tag (${workInProgress.tag}). This error is likely caused by a bug in ` +
'React. Please file an issue.',
);
}
updateHostRoot
will process the updateQueue of our top level root object,
which will result in a new tree to be rendered. Which is what we will see in the
next section.