How Commit works
The commit will occur when render finishes either for the first time from
root.render()
or on updates. We will discuss first when coming from the first
render.
How finishing concurrent render works
After your initial application render and all the mechanics we've seen before, React will call:
finishConcurrentRender(root, exitStatus, finishedWork, lanes);
Signature
finishConcurrentRender
is defined as follows:
function finishConcurrentRender(
root: FiberRoot,
exitStatus: RootExitStatus,
finishedWork: Fiber,
lanes: Lanes,
) {
// [...code]
commitRootWhenReady(
root,
finishedWork,
workInProgressRootRecoverableErrors,
workInProgressTransitions,
lanes,
);
// [...code]
}
Implementation steps
Switch over the
exitStatus
:switch(existStatus) {
case RootInProgress:
case RootFatalErrored: {
// defensive guard
throw new Error('Root did not complete. This is a bug in React.');
}
case RootErrored:
case RootSuspended:
case RootCompleted: {
// continue function execution
break;
}
default: {
throw new Error('Unknown root exit status.');
}
case RootSuspendedWithDelay: {
if (includesOnlyTransitions(lanes)) {
markRootSuspended(root, lanes);
// this function will only mark root suspended and quit in this case
return;
}
break;
}
}When root is suspended and only on retries, then:
if (
includesOnlyRetries(lanes) &&
(alwaysThrottleRetries || exitStatus === RootSuspended)
) {
// ...
}- Compute the milliseconds until timeout and ignore timeouts less than 10 millis
const msUntilTimeout = globalMostRecentFallbackTime + FALLBACK_THROTTLE_MS - now();
if (msUntilTimeout > 10) {
// ...
} - Compute
nextLanes
and quit the function if there is no work; remember, this path is when the render suspended and only when React is retrying the render.const nextLanes = getNextLanes(root, NoLanes);
if (nextLanes !== NoLanes) {
return;
} - schedule a timeout via
setTimeout
to commit the root when ready and quit:This will commit the root to show the suspense fallback 😉root.timeoutHandle = scheduleTimeout(
commitRootWhenReady.bind(
null,
root,
finishedWork,
workInProgressRootRecoverableErrors,
workInProgressTransitions,
lanes,
),
msUntilTimeout,
);
return;
- Compute the milliseconds until timeout and ignore timeouts less than 10 millis
Otherwise, when root isn't suspended and completed successfully, then it will call
commitRootWhenReady
right away:commitRootWhenReady(
root,
finishedWork,
workInProgressRootRecoverableErrors,
workInProgressTransitions,
lanes,
);
How Commit work when ready works
The real commit happens at the commitRoot
function that we will see in a few,
but it is always called from commitRootWhenReady
for concurrent renders,
sync render from performSyncWorkOnRoot
will call commitRoot
independently.
Signature
commitRootWhenReady
is defined as follows
function commitRootWhenReady(
root: FiberRoot,
finishedWork: Fiber,
recoverableErrors: Array<CapturedValue<mixed>> | null,
transitions: Array<Transition> | null,
lanes: Lanes,
) {
// ...
}
Implementation steps
This function will call commitRoot immediately if it is called with an urgent lane:
if (includesOnlyNonUrgentLanes(lanes)) {
// ... code
return;
}
commitRoot(root, recoverableErrors, transitions);
Urgent lanes are as per the definition:
const UrgentLanes = SyncLane | InputContinuousLane | DefaultLane;
return (lanes & UrgentLanes) === NoLanes;
SyncLane
InputContinuousLane
DefaultLane
So coming from root.render() will be in a DefaultLane and thus considered as
urgent and will call commitRoot
.
When all lanes aren't urgent, React will perform a Suspensey commit
by
scheduling the commit for later:
if (includesOnlyNonUrgentLanes(lanes)) {
startSuspendingCommit();
accumulateSuspenseyCommit(finishedWork);
const schedulePendingCommit = waitForCommitToBeReady();
if (schedulePendingCommit !== null) {
root.cancelPendingCommit = schedulePendingCommit(
commitRoot.bind(null, root, recoverableErrors, transitions),
);
markRootSuspended(root, lanes);
return;
}
}
The previous code isn't explained in this section to keep it short. If you are curious about it and want it explained, please open an issue.
How commitRoot
works:
commitRoot
itself will make a call to commitRootImpl
like this:
function commitRoot(
root: FiberRoot,
recoverableErrors: null | Array<CapturedValue<mixed>>,
transitions: Array<Transition> | null,
) {
const previousUpdateLanePriority = getCurrentUpdatePriority();
const prevTransition = ReactCurrentBatchConfig.transition;
try {
ReactCurrentBatchConfig.transition = null;
setCurrentUpdatePriority(DiscreteEventPriority);
commitRootImpl(
root,
recoverableErrors,
transitions,
previousUpdateLanePriority,
);
} finally {
ReactCurrentBatchConfig.transition = prevTransition;
setCurrentUpdatePriority(previousUpdateLanePriority);
}
return null;
}
The commitRootImpl
function
is very long and complex and will call many other functions and many recursions.
It will be a very long section full of information and complex code, get ready!
Here is a very long simplified version of what we will see next (Sorry, I had to
put it in a details
tag):
Simplified long commitRootImpl
// simplified, a lot
function commitRootImpl(
root: FiberRoot,
recoverableErrors: null | Array<CapturedValue<mixed>>,
transitions: Array<Transition> | null,
renderPriorityLevel: EventPriority,
) {
do {
// well, commitRoot may be triggerred while we have a scheduled pending
// effects processing.
// in this case, we need to pass over them now.
flushPassiveEffects();
} while (rootWithPendingPassiveEffects !== null);
if ((executionContext & (RenderContext | CommitContext)) !== NoContext) {
throw new Error('Should not already be working.');
}
const finishedWork = root.finishedWork;
const lanes = root.finishedLanes;
if (finishedWork === null) {
return null;
}
root.finishedWork = null;
root.finishedLanes = NoLanes;
if (finishedWork === root.current) {
throw new Error(
'Cannot commit the same tree as before. This error is likely caused by ' +
'a bug in React. Please file an issue.',
);
}
root.callbackNode = null;
root.callbackPriority = NoLane;
root.cancelPendingCommit = null;
let remainingLanes = mergeLanes(
finishedWork.lanes | finishedWork.childLanes,
getConcurrentlyUpdatedLanes(),
);
markRootFinished(root, remainingLanes);
if (root === workInProgressRoot) {
workInProgressRoot = null;
workInProgress = null;
workInProgressRootRenderLanes = NoLanes;
}
if (
(finishedWork.subtreeFlags & PassiveMask) !== NoFlags ||
(finishedWork.flags & PassiveMask) !== NoFlags
) {
if (!rootDoesHavePassiveEffects) {
rootDoesHavePassiveEffects = true;
pendingPassiveEffectsRemainingLanes = remainingLanes;
pendingPassiveTransitions = transitions;
scheduleCallback(NormalSchedulerPriority, () => {
flushPassiveEffects();
return null;
});
}
}
const subtreeHasEffects =
(finishedWork.subtreeFlags &
(BeforeMutationMask | MutationMask | LayoutMask | PassiveMask)) !==
NoFlags;
const rootHasEffect =
(finishedWork.flags &
(BeforeMutationMask | MutationMask | LayoutMask | PassiveMask)) !==
NoFlags;
if (subtreeHasEffects || rootHasEffect) {
const prevTransition = ReactCurrentBatchConfig.transition;
ReactCurrentBatchConfig.transition = null;
const previousPriority = getCurrentUpdatePriority();
setCurrentUpdatePriority(DiscreteEventPriority);
const prevExecutionContext = executionContext;
executionContext |= CommitContext;
ReactCurrentOwner.current = null;
const shouldFireAfterActiveInstanceBlur = commitBeforeMutationEffects(
root,
finishedWork,
);
// The next phase is the mutation phase, where we mutate the host tree.
commitMutationEffects(root, finishedWork, lanes);
resetAfterCommit(root.containerInfo);
root.current = finishedWork;
commitLayoutEffects(finishedWork, root, lanes);
requestPaint();
executionContext = prevExecutionContext;
setCurrentUpdatePriority(previousPriority);
ReactCurrentBatchConfig.transition = prevTransition;
} else {
// No effects.
root.current = finishedWork;
}
const rootDidHavePassiveEffects = rootDoesHavePassiveEffects;
if (rootDoesHavePassiveEffects) {
rootDoesHavePassiveEffects = false;
rootWithPendingPassiveEffects = root;
pendingPassiveEffectsLanes = lanes;
} else {
releaseRootPooledCache(root, remainingLanes);
}
// Read this again, since an effect might have updated it
remainingLanes = root.pendingLanes;
if (remainingLanes === NoLanes) {
legacyErrorBoundariesThatAlreadyFailed = null;
}
ensureRootIsScheduled(root);
if (recoverableErrors !== null) {
// remember this? createRoot options 😉
const onRecoverableError = root.onRecoverableError;
for (let i = 0; i < recoverableErrors.length; i++) {
const recoverableError = recoverableErrors[i];
const errorInfo = makeErrorInfo(
recoverableError.digest,
recoverableError.stack,
);
onRecoverableError(recoverableError.value, errorInfo);
}
}
if (hasUncaughtError) {
hasUncaughtError = false;
const error = firstUncaughtError;
firstUncaughtError = null;
throw error;
}
if (includesSyncLane(pendingPassiveEffectsLanes) && root.tag !== LegacyRoot) {
flushPassiveEffects();
}
// Read this again, since a passive effect might have updated it
remainingLanes = root.pendingLanes;
if (includesSyncLane(remainingLanes)) {
if (root === rootWithNestedUpdates) {
nestedUpdateCount++;
} else {
nestedUpdateCount = 0;
rootWithNestedUpdates = root;
}
} else {
nestedUpdateCount = 0;
}
flushSyncWorkOnAllRoots();
return null;
}
Let's put this into human-readable words:
This step's purpose is to invoke many stages of effects that will result in displaying the result of your application on the screen.
Reset some root
properties
const finishedWork = root.finishedWork;
root.finishedWork = null;
root.finishedLanes = NoLanes;
root.callbackNode = null;
root.callbackPriority = NoLane;
root.cancelPendingCommit = null;
root.current = finishedWork;
if (root === workInProgressRoot) {
workInProgressRoot = null;
workInProgress = null;
workInProgressRootRenderLanes = NoLanes;
}
// later
Schedule passive effects if applied
React smartly tags the tree to know whether it has effects on it, and when they exist, it will schedule a callback to process them later
if (
(finishedWork.subtreeFlags & PassiveMask) !== NoFlags ||
(finishedWork.flags & PassiveMask) !== NoFlags
) {
if (!rootDoesHavePassiveEffects) {
rootDoesHavePassiveEffects = true;
pendingPassiveEffectsRemainingLanes = remainingLanes;
pendingPassiveTransitions = transitions;
scheduleCallback(NormalSchedulerPriority, () => {
flushPassiveEffects();
return null;
});
}
}
This callback will be executed asynchronously which will leave the time for React to execute other effects types.
Execute Effects
React supports many types of effects, so they are assembled and executed here.
const subtreeHasEffects =
(finishedWork.subtreeFlags &
(BeforeMutationMask | MutationMask | LayoutMask | PassiveMask)) !==
NoFlags;
const rootHasEffect =
(finishedWork.flags &
(BeforeMutationMask | MutationMask | LayoutMask | PassiveMask)) !==
NoFlags;
if (rootHasEffect || subtreeHasEffects) {
const prevTransition = ReactCurrentBatchConfig.transition;
ReactCurrentBatchConfig.transition = null;
const previousPriority = getCurrentUpdatePriority();
setCurrentUpdatePriority(DiscreteEventPriority);
const prevExecutionContext = executionContext;
executionContext |= CommitContext;
ReactCurrentOwner.current = null;
commitBeforeMutationEffects(root, finishedWork);
commitMutationEffects(root, finishedWork, lanes);
resetAfterCommit(root.containerInfo);
root.current = finishedWork;
commitLayoutEffects(finishedWork, root, lanes);
requestPaint();
executionContext = prevExecutionContext;
setCurrentUpdatePriority(previousPriority);
ReactCurrentBatchConfig.transition = prevTransition;
}
if (recoverableErrors !== null) {
// createRoot onRecoverableError option
callRootOnRecoverableErrors(root);
}
if (hasUncaughtError) {
hasUncaughtError = false;
const error = firstUncaughtError;
firstUncaughtError = null;
throw error;
}
if (includesSyncLane(pendingPassiveEffectsLanes) && root.tag !== LegacyRoot) {
flushPassiveEffects();
}
// [...] rest of commit root
React has and will execute the following effects in order:
1. Before mutation effects
This effect will trigger lifecycle events work before mutating the previous tree.
It is used for example to call getSnapshotBeforeUpdate
for class components and/or the experimental useEffectEvent
.
The full switch work can be found here
2. Mutation effects
Mutation effects will the perform and execute the following:
- Deletion effects: Call deleted components effects cleanups.
- Reconciliation effects: Insert the new dom nodes in their correct places.
- Update effects: Or mutation effects. React here will update the dom node with new values from the current render.
- Insertion effects cleanups:
useInsertionEffect
cleanup. - Insertion effects:
useInsertionEffect
. - Layout effects cleanups: Call Layout effects cleanups. Read Andrew's comment to know more.
Gotchas:
- Layout and passive effects are executed
bottom-to-up
. - Layout and passive effects cleanup are executed
up-to-bottom
. - Layout and passive effects cleanup are executed as a whole before the actual effects: loop and call the cleanups from the up to bottom, then when you finish cleanups, call the effects from bottom to up.
useInsertionEffect
doesn't follow the same order as layout or passive effectsuseInsertionEffect
cleanup and actual effect are executed both for every component in abottom-to-up
direction.
3. Layout effects
Layout effects are executed in a sync way after render, so it will be blocking the browser main's thread from painting even if we wrote the new values to our new dom nodes.
During layout effects, React will do the work depending on the type of the component:
- For FunctionComponents:
useLayoutEffect
- For ClassComponents:
componentDidMount
,componentDidUpdate
- Attach
Ref
when applied
4. Passive effects
Yes, passive effects will be invoked in a sync way if you rendered in a SyncLane
.
This is not pretty common, and we've been told (same for componentDidMount
and
update).
If the render occurred in a non-SyncLane
, then the passive effects
will be executed as scheduled previously in the commitRootImpl
.