Skip to main content

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

  1. 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;
    }
    }
  2. When root is suspended and only on retries, then:

    if (
    includesOnlyRetries(lanes) &&
    (alwaysThrottleRetries || exitStatus === RootSuspended)
    ) {
    // ...
    }
    1. Compute the milliseconds until timeout and ignore timeouts less than 10 millis
      const msUntilTimeout = globalMostRecentFallbackTime + FALLBACK_THROTTLE_MS - now();
      if (msUntilTimeout > 10) {
      // ...
      }
    2. 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;
      }
    3. schedule a timeout via setTimeout to commit the root when ready and quit:
      root.timeoutHandle = scheduleTimeout(
      commitRootWhenReady.bind(
      null,
      root,
      finishedWork,
      workInProgressRootRecoverableErrors,
      workInProgressTransitions,
      lanes,
      ),
      msUntilTimeout,
      );
      return;
      This will commit the root to show the suspense fallback 😉
  3. 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:

  1. Deletion effects: Call deleted components effects cleanups.
  2. Reconciliation effects: Insert the new dom nodes in their correct places.
  3. Update effects: Or mutation effects. React here will update the dom node with new values from the current render.
  4. Insertion effects cleanups: useInsertionEffect cleanup.
  5. Insertion effects: useInsertionEffect.
  6. Layout effects cleanups: Call Layout effects cleanups. Read Andrew's comment to know more.
tip

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 effects
  • useInsertionEffect cleanup and actual effect are executed both for every component in a bottom-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.