Source: ReactFiberWorkLoop.new.js
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/
import type {Wakeable} from 'shared/ReactTypes';
import type {Fiber, FiberRoot} from './ReactInternalTypes';
import type {Lanes, Lane} from './ReactFiberLane.new';
import type {SuspenseState} from './ReactFiberSuspenseComponent.new';
import type {StackCursor} from './ReactFiberStack.new';
import type {Flags} from './ReactFiberFlags';
import {
warnAboutDeprecatedLifecycles,
enableSuspenseServerRenderer,
replayFailedUnitOfWorkWithInvokeGuardedCallback,
enableCreateEventHandleAPI,
enableProfilerTimer,
enableProfilerCommitHooks,
enableProfilerNestedUpdatePhase,
enableProfilerNestedUpdateScheduledHook,
deferRenderPhaseUpdateToNextBatch,
enableDebugTracing,
enableSchedulingProfiler,
disableSchedulerTimeoutInWorkLoop,
enableStrictEffects,
skipUnmountedBoundaries,
enableUpdaterTracking,
} from 'shared/ReactFeatureFlags';
import ReactSharedInternals from 'shared/ReactSharedInternals';
import invariant from 'shared/invariant';
import {
// Aliased because `act` will override and push to an internal queue
scheduleCallback as Scheduler_scheduleCallback,
cancelCallback as Scheduler_cancelCallback,
shouldYield,
requestPaint,
now,
ImmediatePriority as ImmediateSchedulerPriority,
UserBlockingPriority as UserBlockingSchedulerPriority,
NormalPriority as NormalSchedulerPriority,
IdlePriority as IdleSchedulerPriority,
} from './Scheduler';
import {
flushSyncCallbacks,
flushSyncCallbacksOnlyInLegacyMode,
scheduleSyncCallback,
scheduleLegacySyncCallback,
} from './ReactFiberSyncTaskQueue.new';
import {
logCommitStarted,
logCommitStopped,
logLayoutEffectsStarted,
logLayoutEffectsStopped,
logPassiveEffectsStarted,
logPassiveEffectsStopped,
logRenderStarted,
logRenderStopped,
} from './DebugTracing';
import {
markCommitStarted,
markCommitStopped,
markLayoutEffectsStarted,
markLayoutEffectsStopped,
markPassiveEffectsStarted,
markPassiveEffectsStopped,
markRenderStarted,
markRenderYielded,
markRenderStopped,
} from './SchedulingProfiler';
import {
resetAfterCommit,
scheduleTimeout,
cancelTimeout,
noTimeout,
warnsIfNotActing,
afterActiveInstanceBlur,
clearContainer,
getCurrentEventPriority,
supportsMicrotasks,
errorHydratingContainer,
scheduleMicrotask,
} from './ReactFiberHostConfig';
import {
createWorkInProgress,
assignFiberPropertiesInDEV,
} from './ReactFiber.new';
import {
NoMode,
StrictLegacyMode,
ProfileMode,
ConcurrentMode,
} from './ReactTypeOfMode';
import {
HostRoot,
IndeterminateComponent,
ClassComponent,
SuspenseComponent,
SuspenseListComponent,
FunctionComponent,
ForwardRef,
MemoComponent,
SimpleMemoComponent,
Profiler,
} from './ReactWorkTags';
import {LegacyRoot} from './ReactRootTags';
import {
NoFlags,
Placement,
Incomplete,
HostEffectMask,
Hydrating,
BeforeMutationMask,
MutationMask,
LayoutMask,
PassiveMask,
MountPassiveDev,
MountLayoutDev,
} from './ReactFiberFlags';
import {
NoLanes,
NoLane,
SyncLane,
NoTimestamp,
claimNextTransitionLane,
claimNextRetryLane,
includesSomeLane,
isSubsetOfLanes,
mergeLanes,
removeLanes,
pickArbitraryLane,
includesNonIdleWork,
includesOnlyRetries,
includesOnlyTransitions,
shouldTimeSlice,
getNextLanes,
markStarvedLanesAsExpired,
getLanesToRetrySynchronouslyOnError,
getMostRecentEventTime,
markRootUpdated,
markRootSuspended as markRootSuspended_dontCallThisOneDirectly,
markRootPinged,
markRootEntangled,
markRootFinished,
getHighestPriorityLane,
addFiberToLanesMap,
movePendingFibersToMemoized,
} from './ReactFiberLane.new';
import {
DiscreteEventPriority,
ContinuousEventPriority,
DefaultEventPriority,
IdleEventPriority,
getCurrentUpdatePriority,
setCurrentUpdatePriority,
lowerEventPriority,
lanesToEventPriority,
} from './ReactEventPriorities.new';
import {requestCurrentTransition, NoTransition} from './ReactFiberTransition';
import {beginWork as originalBeginWork} from './ReactFiberBeginWork.new';
import {completeWork} from './ReactFiberCompleteWork.new';
import {unwindWork, unwindInterruptedWork} from './ReactFiberUnwindWork.new';
import {
throwException,
createRootErrorUpdate,
createClassErrorUpdate,
} from './ReactFiberThrow.new';
import {
commitBeforeMutationEffects,
commitLayoutEffects,
commitMutationEffects,
commitPassiveEffectDurations,
commitPassiveMountEffects,
commitPassiveUnmountEffects,
invokeLayoutEffectMountInDEV,
invokePassiveEffectMountInDEV,
invokeLayoutEffectUnmountInDEV,
invokePassiveEffectUnmountInDEV,
} from './ReactFiberCommitWork.new';
import {enqueueUpdate} from './ReactUpdateQueue.new';
import {resetContextDependencies} from './ReactFiberNewContext.new';
import {
resetHooksAfterThrow,
ContextOnlyDispatcher,
getIsUpdatingOpaqueValueInRenderPhaseInDEV,
} from './ReactFiberHooks.new';
import {createCapturedValue} from './ReactCapturedValue';
import {
push as pushToStack,
pop as popFromStack,
createCursor,
} from './ReactFiberStack.new';
import {enqueueInterleavedUpdates} from './ReactFiberInterleavedUpdates.new';
import {
markNestedUpdateScheduled,
recordCommitTime,
resetNestedUpdateFlag,
startProfilerTimer,
stopProfilerTimerIfRunningAndRecordDelta,
syncNestedUpdateFlag,
} from './ReactProfilerTimer.new';
// DEV stuff
import getComponentNameFromFiber from 'react-reconciler/src/getComponentNameFromFiber';
import ReactStrictModeWarnings from './ReactStrictModeWarnings.new';
import {
isRendering as ReactCurrentDebugFiberIsRenderingInDEV,
current as ReactCurrentFiberCurrent,
resetCurrentFiber as resetCurrentDebugFiberInDEV,
setCurrentFiber as setCurrentDebugFiberInDEV,
} from './ReactCurrentFiber';
import {
invokeGuardedCallback,
hasCaughtError,
clearCaughtError,
} from 'shared/ReactErrorUtils';
import {
onCommitRoot as onCommitRootDevTools,
onPostCommitRoot as onPostCommitRootDevTools,
isDevToolsPresent,
} from './ReactFiberDevToolsHook.new';
import {onCommitRoot as onCommitRootTestSelector} from './ReactTestSelectors';
const ceil = Math.ceil;
const {
ReactCurrentDispatcher,
ReactCurrentOwner,
ReactCurrentBatchConfig,
ReactCurrentActQueue,
} = ReactSharedInternals;
type ExecutionContext = number;
export const NoContext = /* */ 0b0000;
const BatchedContext = /* */ 0b0001;
const RenderContext = /* */ 0b0010;
const CommitContext = /* */ 0b0100;
export const RetryAfterError = /* */ 0b1000;
type RootExitStatus = 0 | 1 | 2 | 3 | 4 | 5;
const RootIncomplete = 0;
const RootFatalErrored = 1;
const RootErrored = 2;
const RootSuspended = 3;
const RootSuspendedWithDelay = 4;
const RootCompleted = 5;
// Describes where we are in the React execution stack
let executionContext: ExecutionContext = NoContext;
// The root we're working on
let workInProgressRoot: FiberRoot | null = null;
// The fiber we're working on
let workInProgress: Fiber | null = null;
// The lanes we're rendering
let workInProgressRootRenderLanes: Lanes = NoLanes;
// Stack that allows components to change the render lanes for its subtree
// This is a superset of the lanes we started working on at the root. The only
// case where it's different from `workInProgressRootRenderLanes` is when we
// enter a subtree that is hidden and needs to be unhidden: Suspense and
// Offscreen component.
//
// Most things in the work loop should deal with workInProgressRootRenderLanes.
// Most things in begin/complete phases should deal with subtreeRenderLanes.
export let subtreeRenderLanes: Lanes = NoLanes;
const subtreeRenderLanesCursor: StackCursor<Lanes> = createCursor(NoLanes);
// Whether to root completed, errored, suspended, etc.
let workInProgressRootExitStatus: RootExitStatus = RootIncomplete;
// A fatal error, if one is thrown
let workInProgressRootFatalError: mixed = null;
// "Included" lanes refer to lanes that were worked on during this render. It's
// slightly different than `renderLanes` because `renderLanes` can change as you
// enter and exit an Offscreen tree. This value is the combination of all render
// lanes for the entire render phase.
let workInProgressRootIncludedLanes: Lanes = NoLanes;
// The work left over by components that were visited during this render. Only
// includes unprocessed updates, not work in bailed out children.
let workInProgressRootSkippedLanes: Lanes = NoLanes;
// Lanes that were updated (in an interleaved event) during this render.
let workInProgressRootUpdatedLanes: Lanes = NoLanes;
// Lanes that were pinged (in an interleaved event) during this render.
let workInProgressRootPingedLanes: Lanes = NoLanes;
// The most recent time we committed a fallback. This lets us ensure a train
// model where we don't commit new loading states in too quick succession.
let globalMostRecentFallbackTime: number = 0;
const FALLBACK_THROTTLE_MS: number = 500;
// The absolute time for when we should start giving up on rendering
// more and prefer CPU suspense heuristics instead.
let workInProgressRootRenderTargetTime: number = Infinity;
// How long a render is supposed to take before we start following CPU
// suspense heuristics and opt out of rendering more content.
const RENDER_TIMEOUT_MS = 500;
function resetRenderTimer() {
workInProgressRootRenderTargetTime = now() + RENDER_TIMEOUT_MS;
}
export function getRenderTargetTime(): number {
return workInProgressRootRenderTargetTime;
}
let hasUncaughtError = false;
let firstUncaughtError = null;
let legacyErrorBoundariesThatAlreadyFailed: Set<mixed> | null = null;
// Only used when enableProfilerNestedUpdateScheduledHook is true;
// to track which root is currently committing layout effects.
let rootCommittingMutationOrLayoutEffects: FiberRoot | null = null;
let rootDoesHavePassiveEffects: boolean = false;
let rootWithPendingPassiveEffects: FiberRoot | null = null;
let pendingPassiveEffectsLanes: Lanes = NoLanes;
let pendingPassiveProfilerEffects: Array<Fiber> = [];
// Use these to prevent an infinite loop of nested updates
const NESTED_UPDATE_LIMIT = 50;
let nestedUpdateCount: number = 0;
let rootWithNestedUpdates: FiberRoot | null = null;
const NESTED_PASSIVE_UPDATE_LIMIT = 50;
let nestedPassiveUpdateCount: number = 0;
// If two updates are scheduled within the same event, we should treat their
// event times as simultaneous, even if the actual clock time has advanced
// between the first and second call.
let currentEventTime: number = NoTimestamp;
let currentEventTransitionLane: Lanes = NoLanes;
export function getWorkInProgressRoot(): FiberRoot | null {
return workInProgressRoot;
}
export function requestEventTime() {
if ((executionContext & (RenderContext | CommitContext)) !== NoContext) {
// We're inside React, so it's fine to read the actual time.
return now();
}
// We're not inside React, so we may be in the middle of a browser event.
if (currentEventTime !== NoTimestamp) {
// Use the same start time for all updates until we enter React again.
return currentEventTime;
}
// This is the first update since React yielded. Compute a new start time.
currentEventTime = now();
return currentEventTime;
}
export function getCurrentTime() {
return now();
}
export function requestUpdateLane(fiber: Fiber): Lane {
// Special cases
const mode = fiber.mode;
if ((mode & ConcurrentMode) === NoMode) {
return (SyncLane: Lane);
} else if (
!deferRenderPhaseUpdateToNextBatch &&
(executionContext & RenderContext) !== NoContext &&
workInProgressRootRenderLanes !== NoLanes
) {
// This is a render phase update. These are not officially supported. The
// old behavior is to give this the same "thread" (lanes) as
// whatever is currently rendering. So if you call `setState` on a component
// that happens later in the same render, it will flush. Ideally, we want to
// remove the special case and treat them as if they came from an
// interleaved event. Regardless, this pattern is not officially supported.
// This behavior is only a fallback. The flag only exists until we can roll
// out the setState warning, since existing code might accidentally rely on
// the current behavior.
return pickArbitraryLane(workInProgressRootRenderLanes);
}
const isTransition = requestCurrentTransition() !== NoTransition;
if (isTransition) {
// The algorithm for assigning an update to a lane should be stable for all
// updates at the same priority within the same event. To do this, the
// inputs to the algorithm must be the same.
//
// The trick we use is to cache the first of each of these inputs within an
// event. Then reset the cached values once we can be sure the event is
// over. Our heuristic for that is whenever we enter a concurrent work loop.
if (currentEventTransitionLane === NoLane) {
// All transitions within the same event are assigned the same lane.
currentEventTransitionLane = claimNextTransitionLane();
}
return currentEventTransitionLane;
}
// Updates originating inside certain React methods, like flushSync, have
// their priority set by tracking it with a context variable.
//
// The opaque type returned by the host config is internally a lane, so we can
// use that directly.
// TODO: Move this type conversion to the event priority module.
const updateLane: Lane = (getCurrentUpdatePriority(): any);
if (updateLane !== NoLane) {
return updateLane;
}
// This update originated outside React. Ask the host environment for an
// appropriate priority, based on the type of event.
//
// The opaque type returned by the host config is internally a lane, so we can
// use that directly.
// TODO: Move this type conversion to the event priority module.
const eventLane: Lane = (getCurrentEventPriority(): any);
return eventLane;
}
function requestRetryLane(fiber: Fiber) {
// This is a fork of `requestUpdateLane` designed specifically for Suspense
// "retries" â a special update that attempts to flip a Suspense boundary
// from its placeholder state to its primary/resolved state.
// Special cases
const mode = fiber.mode;
if ((mode & ConcurrentMode) === NoMode) {
return (SyncLane: Lane);
}
return claimNextRetryLane();
}
export function scheduleUpdateOnFiber(
fiber: Fiber,
lane: Lane,
eventTime: number,
): FiberRoot | null {
checkForNestedUpdates();
warnAboutRenderPhaseUpdatesInDEV(fiber);
const root = markUpdateLaneFromFiberToRoot(fiber, lane);
if (root === null) {
return null;
}
if (enableUpdaterTracking) {
if (isDevToolsPresent) {
addFiberToLanesMap(root, fiber, lane);
}
}
// Mark that the root has a pending update.
markRootUpdated(root, lane, eventTime);
if (enableProfilerTimer && enableProfilerNestedUpdateScheduledHook) {
if (
(executionContext & CommitContext) !== NoContext &&
root === rootCommittingMutationOrLayoutEffects
) {
if (fiber.mode & ProfileMode) {
let current = fiber;
while (current !== null) {
if (current.tag === Profiler) {
const {id, onNestedUpdateScheduled} = current.memoizedProps;
if (typeof onNestedUpdateScheduled === 'function') {
onNestedUpdateScheduled(id);
}
}
current = current.return;
}
}
}
}
// TODO: Consolidate with `isInterleavedUpdate` check
if (root === workInProgressRoot) {
// Received an update to a tree that's in the middle of rendering. Mark
// that there was an interleaved update work on this root. Unless the
// `deferRenderPhaseUpdateToNextBatch` flag is off and this is a render
// phase update. In that case, we don't treat render phase updates as if
// they were interleaved, for backwards compat reasons.
if (
deferRenderPhaseUpdateToNextBatch ||
(executionContext & RenderContext) === NoContext
) {
workInProgressRootUpdatedLanes = mergeLanes(
workInProgressRootUpdatedLanes,
lane,
);
}
if (workInProgressRootExitStatus === RootSuspendedWithDelay) {
// The root already suspended with a delay, which means this render
// definitely won't finish. Since we have a new update, let's mark it as
// suspended now, right before marking the incoming update. This has the
// effect of interrupting the current render and switching to the update.
// TODO: Make sure this doesn't override pings that happen while we've
// already started rendering.
markRootSuspended(root, workInProgressRootRenderLanes);
}
}
ensureRootIsScheduled(root, eventTime);
if (
lane === SyncLane &&
executionContext === NoContext &&
(fiber.mode & ConcurrentMode) === NoMode &&
// Treat `act` as if it's inside `batchedUpdates`, even in legacy mode.
!(__DEV__ && ReactCurrentActQueue.isBatchingLegacy)
) {
// Flush the synchronous work now, unless we're already working or inside
// a batch. This is intentionally inside scheduleUpdateOnFiber instead of
// scheduleCallbackForFiber to preserve the ability to schedule a callback
// without immediately flushing it. We only do this for user-initiated
// updates, to preserve historical behavior of legacy mode.
resetRenderTimer();
flushSyncCallbacksOnlyInLegacyMode();
}
return root;
}
// This is split into a separate function so we can mark a fiber with pending
// work without treating it as a typical update that originates from an event;
// e.g. retrying a Suspense boundary isn't an update, but it does schedule work
// on a fiber.
function markUpdateLaneFromFiberToRoot(
sourceFiber: Fiber,
lane: Lane,
): FiberRoot | null {
// Update the source fiber's lanes
sourceFiber.lanes = mergeLanes(sourceFiber.lanes, lane);
let alternate = sourceFiber.alternate;
if (alternate !== null) {
alternate.lanes = mergeLanes(alternate.lanes, lane);
}
if (__DEV__) {
if (
alternate === null &&
(sourceFiber.flags & (Placement | Hydrating)) !== NoFlags
) {
warnAboutUpdateOnNotYetMountedFiberInDEV(sourceFiber);
}
}
// Walk the parent path to the root and update the child lanes.
let node = sourceFiber;
let parent = sourceFiber.return;
while (parent !== null) {
parent.childLanes = mergeLanes(parent.childLanes, lane);
alternate = parent.alternate;
if (alternate !== null) {
alternate.childLanes = mergeLanes(alternate.childLanes, lane);
} else {
if (__DEV__) {
if ((parent.flags & (Placement | Hydrating)) !== NoFlags) {
warnAboutUpdateOnNotYetMountedFiberInDEV(sourceFiber);
}
}
}
node = parent;
parent = parent.return;
}
if (node.tag === HostRoot) {
const root: FiberRoot = node.stateNode;
return root;
} else {
return null;
}
}
export function isInterleavedUpdate(fiber: Fiber, lane: Lane) {
return (
// TODO: Optimize slightly by comparing to root that fiber belongs to.
// Requires some refactoring. Not a big deal though since it's rare for
// concurrent apps to have more than a single root.
workInProgressRoot !== null &&
(fiber.mode & ConcurrentMode) !== NoMode &&
// If this is a render phase update (i.e. UNSAFE_componentWillReceiveProps),
// then don't treat this as an interleaved update. This pattern is
// accompanied by a warning but we haven't fully deprecated it yet. We can
// remove once the deferRenderPhaseUpdateToNextBatch flag is enabled.
(deferRenderPhaseUpdateToNextBatch ||
(executionContext & RenderContext) === NoContext)
);
}
// Use this function to schedule a task for a root. There's only one task per
// root; if a task was already scheduled, we'll check to make sure the priority
// of the existing task is the same as the priority of the next level that the
// root has work on. This function is called on every update, and right before
// exiting a task.
function ensureRootIsScheduled(root: FiberRoot, currentTime: number) {
const existingCallbackNode = root.callbackNode;
// Check if any lanes are being starved by other work. If so, mark them as
// expired so we know to work on those next.
markStarvedLanesAsExpired(root, currentTime);
// Determine the next lanes to work on, and their priority.
const nextLanes = getNextLanes(
root,
root === workInProgressRoot ? workInProgressRootRenderLanes : NoLanes,
);
if (nextLanes === NoLanes) {
// Special case: There's nothing to work on.
if (existingCallbackNode !== null) {
cancelCallback(existingCallbackNode);
}
root.callbackNode = null;
root.callbackPriority = NoLane;
return;
}
// We use the highest priority lane to represent the priority of the callback.
const newCallbackPriority = getHighestPriorityLane(nextLanes);
// Check if there's an existing task. We may be able to reuse it.
const existingCallbackPriority = root.callbackPriority;
if (
existingCallbackPriority === newCallbackPriority &&
// Special case related to `act`. If the currently scheduled task is a
// Scheduler task, rather than an `act` task, cancel it and re-scheduled
// on the `act` queue.
!(
__DEV__ &&
ReactCurrentActQueue.current !== null &&
existingCallbackNode !== fakeActCallbackNode
)
) {
if (__DEV__) {
// If we're going to re-use an existing task, it needs to exist.
// Assume that discrete update microtasks are non-cancellable and null.
// TODO: Temporary until we confirm this warning is not fired.
if (
existingCallbackNode == null &&
existingCallbackPriority !== SyncLane
) {
console.error(
'Expected scheduled callback to exist. This error is likely caused by a bug in React. Please file an issue.',
);
}
}
// The priority hasn't changed. We can reuse the existing task. Exit.
return;
}
if (existingCallbackNode != null) {
// Cancel the existing callback. We'll schedule a new one below.
cancelCallback(existingCallbackNode);
}
// Schedule a new callback.
let newCallbackNode;
if (newCallbackPriority === SyncLane) {
// Special case: Sync React callbacks are scheduled on a special
// internal queue
if (root.tag === LegacyRoot) {
if (__DEV__ && ReactCurrentActQueue.isBatchingLegacy !== null) {
ReactCurrentActQueue.didScheduleLegacyUpdate = true;
}
scheduleLegacySyncCallback(performSyncWorkOnRoot.bind(null, root));
} else {
scheduleSyncCallback(performSyncWorkOnRoot.bind(null, root));
}
if (supportsMicrotasks) {
// Flush the queue in a microtask.
if (__DEV__ && ReactCurrentActQueue.current !== null) {
// Inside `act`, use our internal `act` queue so that these get flushed
// at the end of the current scope even when using the sync version
// of `act`.
ReactCurrentActQueue.current.push(flushSyncCallbacks);
} else {
scheduleMicrotask(flushSyncCallbacks);
}
} else {
// Flush the queue in an Immediate task.
scheduleCallback(ImmediateSchedulerPriority, flushSyncCallbacks);
}
newCallbackNode = null;
} else {
let schedulerPriorityLevel;
switch (lanesToEventPriority(nextLanes)) {
case DiscreteEventPriority:
schedulerPriorityLevel = ImmediateSchedulerPriority;
break;
case ContinuousEventPriority:
schedulerPriorityLevel = UserBlockingSchedulerPriority;
break;
case DefaultEventPriority:
schedulerPriorityLevel = NormalSchedulerPriority;
break;
case IdleEventPriority:
schedulerPriorityLevel = IdleSchedulerPriority;
break;
default:
schedulerPriorityLevel = NormalSchedulerPriority;
break;
}
newCallbackNode = scheduleCallback(
schedulerPriorityLevel,
performConcurrentWorkOnRoot.bind(null, root),
);
}
root.callbackPriority = newCallbackPriority;
root.callbackNode = newCallbackNode;
}
// This is the entry point for every concurrent task, i.e. anything that
// goes through Scheduler.
function performConcurrentWorkOnRoot(root, didTimeout) {
if (enableProfilerTimer && enableProfilerNestedUpdatePhase) {
resetNestedUpdateFlag();
}
// Since we know we're in a React event, we can clear the current
// event time. The next update will compute a new event time.
currentEventTime = NoTimestamp;
currentEventTransitionLane = NoLanes;
invariant(
(executionContext & (RenderContext | CommitContext)) === NoContext,
'Should not already be working.',
);
// Flush any pending passive effects before deciding which lanes to work on,
// in case they schedule additional work.
const originalCallbackNode = root.callbackNode;
const didFlushPassiveEffects = flushPassiveEffects();
if (didFlushPassiveEffects) {
// Something in the passive effect phase may have canceled the current task.
// Check if the task node for this root was changed.
if (root.callbackNode !== originalCallbackNode) {
// The current task was canceled. Exit. We don't need to call
// `ensureRootIsScheduled` because the check above implies either that
// there's a new task, or that there's no remaining work on this root.
return null;
} else {
// Current task was not canceled. Continue.
}
}
// Determine the next lanes to work on, using the fields stored
// on the root.
let lanes = getNextLanes(
root,
root === workInProgressRoot ? workInProgressRootRenderLanes : NoLanes,
);
if (lanes === NoLanes) {
// Defensive coding. This is never expected to happen.
return null;
}
// We disable time-slicing in some cases: if the work has been CPU-bound
// for too long ("expired" work, to prevent starvation), or we're in
// sync-updates-by-default mode.
// TODO: We only check `didTimeout` defensively, to account for a Scheduler
// bug we're still investigating. Once the bug in Scheduler is fixed,
// we can remove this, since we track expiration ourselves.
let exitStatus =
shouldTimeSlice(root, lanes) &&
(disableSchedulerTimeoutInWorkLoop || !didTimeout)
? renderRootConcurrent(root, lanes)
: renderRootSync(root, lanes);
if (exitStatus !== RootIncomplete) {
if (exitStatus === RootErrored) {
const prevExecutionContext = executionContext;
executionContext |= RetryAfterError;
// If an error occurred during hydration,
// discard server response and fall back to client side render.
if (root.hydrate) {
root.hydrate = false;
if (__DEV__) {
errorHydratingContainer(root.containerInfo);
}
clearContainer(root.containerInfo);
}
// If something threw an error, try rendering one more time. We'll render
// synchronously to block concurrent data mutations, and we'll includes
// all pending updates are included. If it still fails after the second
// attempt, we'll give up and commit the resulting tree.
const errorRetryLanes = getLanesToRetrySynchronouslyOnError(root);
if (errorRetryLanes !== NoLanes) {
lanes = errorRetryLanes;
exitStatus = renderRootSync(root, errorRetryLanes);
}
executionContext = prevExecutionContext;
}
if (exitStatus === RootFatalErrored) {
const fatalError = workInProgressRootFatalError;
prepareFreshStack(root, NoLanes);
markRootSuspended(root, lanes);
ensureRootIsScheduled(root, now());
throw fatalError;
}
// We now have a consistent tree. The next step is either to commit it,
// or, if something suspended, wait to commit it after a timeout.
const finishedWork: Fiber = (root.current.alternate: any);
root.finishedWork = finishedWork;
root.finishedLanes = lanes;
finishConcurrentRender(root, exitStatus, lanes);
}
ensureRootIsScheduled(root, now());
if (root.callbackNode === originalCallbackNode) {
// The task node scheduled for this root is the same one that's
// currently executed. Need to return a continuation.
return performConcurrentWorkOnRoot.bind(null, root);
}
return null;
}
function finishConcurrentRender(root, exitStatus, lanes) {
switch (exitStatus) {
case RootIncomplete:
case RootFatalErrored: {
invariant(false, 'Root did not complete. This is a bug in React.');
}
// Flow knows about invariant, so it complains if I add a break
// statement, but eslint doesn't know about invariant, so it complains
// if I do. eslint-disable-next-line no-fallthrough
case RootErrored: {
// We should have already attempted to retry this tree. If we reached
// this point, it errored again. Commit it.
commitRoot(root);
break;
}
case RootSuspended: {
markRootSuspended(root, lanes);
// We have an acceptable loading state. We need to figure out if we
// should immediately commit it or wait a bit.
if (
includesOnlyRetries(lanes) &&
// do not delay if we're inside an act() scope
!shouldForceFlushFallbacksInDEV()
) {
// This render only included retries, no updates. Throttle committing
// retries so that we don't show too many loading states too quickly.
const msUntilTimeout =
globalMostRecentFallbackTime + FALLBACK_THROTTLE_MS - now();
// Don't bother with a very short suspense time.
if (msUntilTimeout > 10) {
const nextLanes = getNextLanes(root, NoLanes);
if (nextLanes !== NoLanes) {
// There's additional work on this root.
break;
}
const suspendedLanes = root.suspendedLanes;
if (!isSubsetOfLanes(suspendedLanes, lanes)) {
// We should prefer to render the fallback of at the last
// suspended level. Ping the last suspended level to try
// rendering it again.
// FIXME: What if the suspended lanes are Idle? Should not restart.
const eventTime = requestEventTime();
markRootPinged(root, suspendedLanes, eventTime);
break;
}
// The render is suspended, it hasn't timed out, and there's no
// lower priority work to do. Instead of committing the fallback
// immediately, wait for more data to arrive.
root.timeoutHandle = scheduleTimeout(
commitRoot.bind(null, root),
msUntilTimeout,
);
break;
}
}
// The work expired. Commit immediately.
commitRoot(root);
break;
}
case RootSuspendedWithDelay: {
markRootSuspended(root, lanes);
if (includesOnlyTransitions(lanes)) {
// This is a transition, so we should exit without committing a
// placeholder and without scheduling a timeout. Delay indefinitely
// until we receive more data.
break;
}
if (!shouldForceFlushFallbacksInDEV()) {
// This is not a transition, but we did trigger an avoided state.
// Schedule a placeholder to display after a short delay, using the Just
// Noticeable Difference.
// TODO: Is the JND optimization worth the added complexity? If this is
// the only reason we track the event time, then probably not.
// Consider removing.
const mostRecentEventTime = getMostRecentEventTime(root, lanes);
const eventTimeMs = mostRecentEventTime;
const timeElapsedMs = now() - eventTimeMs;
const msUntilTimeout = jnd(timeElapsedMs) - timeElapsedMs;
// Don't bother with a very short suspense time.
if (msUntilTimeout > 10) {
// Instead of committing the fallback immediately, wait for more data
// to arrive.
root.timeoutHandle = scheduleTimeout(
commitRoot.bind(null, root),
msUntilTimeout,
);
break;
}
}
// Commit the placeholder.
commitRoot(root);
break;
}
case RootCompleted: {
// The work completed. Ready to commit.
commitRoot(root);
break;
}
default: {
invariant(false, 'Unknown root exit status.');
}
}
}
function markRootSuspended(root, suspendedLanes) {
// When suspending, we should always exclude lanes that were pinged or (more
// rarely, since we try to avoid it) updated during the render phase.
// TODO: Lol maybe there's a better way to factor this besides this
// obnoxiously named function :)
suspendedLanes = removeLanes(suspendedLanes, workInProgressRootPingedLanes);
suspendedLanes = removeLanes(suspendedLanes, workInProgressRootUpdatedLanes);
markRootSuspended_dontCallThisOneDirectly(root, suspendedLanes);
}
// This is the entry point for synchronous tasks that don't go
// through Scheduler
function performSyncWorkOnRoot(root) {
if (enableProfilerTimer && enableProfilerNestedUpdatePhase) {
syncNestedUpdateFlag();
}
invariant(
(executionContext & (RenderContext | CommitContext)) === NoContext,
'Should not already be working.',
);
flushPassiveEffects();
let lanes = getNextLanes(root, NoLanes);
if (!includesSomeLane(lanes, SyncLane)) {
// There's no remaining sync work left.
ensureRootIsScheduled(root, now());
return null;
}
let exitStatus = renderRootSync(root, lanes);
if (root.tag !== LegacyRoot && exitStatus === RootErrored) {
const prevExecutionContext = executionContext;
executionContext |= RetryAfterError;
// If an error occurred during hydration,
// discard server response and fall back to client side render.
if (root.hydrate) {
root.hydrate = false;
if (__DEV__) {
errorHydratingContainer(root.containerInfo);
}
clearContainer(root.containerInfo);
}
// If something threw an error, try rendering one more time. We'll render
// synchronously to block concurrent data mutations, and we'll includes
// all pending updates are included. If it still fails after the second
// attempt, we'll give up and commit the resulting tree.
const errorRetryLanes = getLanesToRetrySynchronouslyOnError(root);
if (errorRetryLanes !== NoLanes) {
lanes = errorRetryLanes;
exitStatus = renderRootSync(root, lanes);
}
executionContext = prevExecutionContext;
}
if (exitStatus === RootFatalErrored) {
const fatalError = workInProgressRootFatalError;
prepareFreshStack(root, NoLanes);
markRootSuspended(root, lanes);
ensureRootIsScheduled(root, now());
throw fatalError;
}
// We now have a consistent tree. Because this is a sync render, we
// will commit it even if something suspended.
const finishedWork: Fiber = (root.current.alternate: any);
root.finishedWork = finishedWork;
root.finishedLanes = lanes;
commitRoot(root);
// Before exiting, make sure there's a callback scheduled for the next
// pending level.
ensureRootIsScheduled(root, now());
return null;
}
export function flushRoot(root: FiberRoot, lanes: Lanes) {
if (lanes !== NoLanes) {
markRootEntangled(root, mergeLanes(lanes, SyncLane));
ensureRootIsScheduled(root, now());
if ((executionContext & (RenderContext | CommitContext)) === NoContext) {
resetRenderTimer();
flushSyncCallbacks();
}
}
}
export function getExecutionContext(): ExecutionContext {
return executionContext;
}
export function deferredUpdates<A>(fn: () => A): A {
const previousPriority = getCurrentUpdatePriority();
const prevTransition = ReactCurrentBatchConfig.transition;
try {
ReactCurrentBatchConfig.transition = 0;
setCurrentUpdatePriority(DefaultEventPriority);
return fn();
} finally {
setCurrentUpdatePriority(previousPriority);
ReactCurrentBatchConfig.transition = prevTransition;
}
}
export function batchedUpdates<A, R>(fn: A => R, a: A): R {
const prevExecutionContext = executionContext;
executionContext |= BatchedContext;
try {
return fn(a);
} finally {
executionContext = prevExecutionContext;
// If there were legacy sync updates, flush them at the end of the outer
// most batchedUpdates-like method.
if (
executionContext === NoContext &&
// Treat `act` as if it's inside `batchedUpdates`, even in legacy mode.
!(__DEV__ && ReactCurrentActQueue.isBatchingLegacy)
) {
resetRenderTimer();
flushSyncCallbacksOnlyInLegacyMode();
}
}
}
export function discreteUpdates<A, B, C, D, R>(
fn: (A, B, C, D) => R,
a: A,
b: B,
c: C,
d: D,
): R {
const previousPriority = getCurrentUpdatePriority();
const prevTransition = ReactCurrentBatchConfig.transition;
try {
ReactCurrentBatchConfig.transition = 0;
setCurrentUpdatePriority(DiscreteEventPriority);
return fn(a, b, c, d);
} finally {
setCurrentUpdatePriority(previousPriority);
ReactCurrentBatchConfig.transition = prevTransition;
if (executionContext === NoContext) {
resetRenderTimer();
}
}
}
// Overload the definition to the two valid signatures.
// Warning, this opts-out of checking the function body.
declare function flushSyncWithoutWarningIfAlreadyRendering<R>(fn: () => R): R;
// eslint-disable-next-line no-redeclare
declare function flushSyncWithoutWarningIfAlreadyRendering(): void;
// eslint-disable-next-line no-redeclare
export function flushSyncWithoutWarningIfAlreadyRendering(fn) {
// In legacy mode, we flush pending passive effects at the beginning of the
// next event, not at the end of the previous one.
if (
rootWithPendingPassiveEffects !== null &&
rootWithPendingPassiveEffects.tag === LegacyRoot &&
(executionContext & (RenderContext | CommitContext)) === NoContext
) {
flushPassiveEffects();
}
const prevExecutionContext = executionContext;
executionContext |= BatchedContext;
const prevTransition = ReactCurrentBatchConfig.transition;
const previousPriority = getCurrentUpdatePriority();
try {
ReactCurrentBatchConfig.transition = 0;
setCurrentUpdatePriority(DiscreteEventPriority);
if (fn) {
return fn();
} else {
return undefined;
}
} finally {
setCurrentUpdatePriority(previousPriority);
ReactCurrentBatchConfig.transition = prevTransition;
executionContext = prevExecutionContext;
// Flush the immediate callbacks that were scheduled during this batch.
// Note that this will happen even if batchedUpdates is higher up
// the stack.
if ((executionContext & (RenderContext | CommitContext)) === NoContext) {
flushSyncCallbacks();
}
}
}
// Overload the definition to the two valid signatures.
// Warning, this opts-out of checking the function body.
declare function flushSync<R>(fn: () => R): R;
// eslint-disable-next-line no-redeclare
declare function flushSync(): void;
// eslint-disable-next-line no-redeclare
export function flushSync(fn) {
if (__DEV__) {
if ((executionContext & (RenderContext | CommitContext)) !== NoContext) {
console.error(
'flushSync was called from inside a lifecycle method. React cannot ' +
'flush when React is already rendering. Consider moving this call to ' +
'a scheduler task or micro task.',
);
}
}
return flushSyncWithoutWarningIfAlreadyRendering(fn);
}
export function flushControlled(fn: () => mixed): void {
const prevExecutionContext = executionContext;
executionContext |= BatchedContext;
const prevTransition = ReactCurrentBatchConfig.transition;
const previousPriority = getCurrentUpdatePriority();
try {
ReactCurrentBatchConfig.transition = 0;
setCurrentUpdatePriority(DiscreteEventPriority);
fn();
} finally {
setCurrentUpdatePriority(previousPriority);
ReactCurrentBatchConfig.transition = prevTransition;
executionContext = prevExecutionContext;
if (executionContext === NoContext) {
// Flush the immediate callbacks that were scheduled during this batch
resetRenderTimer();
flushSyncCallbacks();
}
}
}
export function pushRenderLanes(fiber: Fiber, lanes: Lanes) {
pushToStack(subtreeRenderLanesCursor, subtreeRenderLanes, fiber);
subtreeRenderLanes = mergeLanes(subtreeRenderLanes, lanes);
workInProgressRootIncludedLanes = mergeLanes(
workInProgressRootIncludedLanes,
lanes,
);
}
export function popRenderLanes(fiber: Fiber) {
subtreeRenderLanes = subtreeRenderLanesCursor.current;
popFromStack(subtreeRenderLanesCursor, fiber);
}
function prepareFreshStack(root: FiberRoot, lanes: Lanes) {
root.finishedWork = null;
root.finishedLanes = NoLanes;
const timeoutHandle = root.timeoutHandle;
if (timeoutHandle !== noTimeout) {
// The root previous suspended and scheduled a timeout to commit a fallback
// state. Now that we have additional work, cancel the timeout.
root.timeoutHandle = noTimeout;
// $FlowFixMe Complains noTimeout is not a TimeoutID, despite the check above
cancelTimeout(timeoutHandle);
}
if (workInProgress !== null) {
let interruptedWork = workInProgress.return;
while (interruptedWork !== null) {
unwindInterruptedWork(interruptedWork, workInProgressRootRenderLanes);
interruptedWork = interruptedWork.return;
}
}
workInProgressRoot = root;
workInProgress = createWorkInProgress(root.current, null);
workInProgressRootRenderLanes = subtreeRenderLanes = workInProgressRootIncludedLanes = lanes;
workInProgressRootExitStatus = RootIncomplete;
workInProgressRootFatalError = null;
workInProgressRootSkippedLanes = NoLanes;
workInProgressRootUpdatedLanes = NoLanes;
workInProgressRootPingedLanes = NoLanes;
enqueueInterleavedUpdates();
if (__DEV__) {
ReactStrictModeWarnings.discardPendingWarnings();
}
}
function handleError(root, thrownValue): void {
do {
let erroredWork = workInProgress;
try {
// Reset module-level state that was set during the render phase.
resetContextDependencies();
resetHooksAfterThrow();
resetCurrentDebugFiberInDEV();
// TODO: I found and added this missing line while investigating a
// separate issue. Write a regression test using string refs.
ReactCurrentOwner.current = null;
if (erroredWork === null || erroredWork.return === null) {
// Expected to be working on a non-root fiber. This is a fatal error
// because there's no ancestor that can handle it; the root is
// supposed to capture all errors that weren't caught by an error
// boundary.
workInProgressRootExitStatus = RootFatalErrored;
workInProgressRootFatalError = thrownValue;
// Set `workInProgress` to null. This represents advancing to the next
// sibling, or the parent if there are no siblings. But since the root
// has no siblings nor a parent, we set it to null. Usually this is
// handled by `completeUnitOfWork` or `unwindWork`, but since we're
// intentionally not calling those, we need set it here.
// TODO: Consider calling `unwindWork` to pop the contexts.
workInProgress = null;
return;
}
if (enableProfilerTimer && erroredWork.mode & ProfileMode) {
// Record the time spent rendering before an error was thrown. This
// avoids inaccurate Profiler durations in the case of a
// suspended render.
stopProfilerTimerIfRunningAndRecordDelta(erroredWork, true);
}
throwException(
root,
erroredWork.return,
erroredWork,
thrownValue,
workInProgressRootRenderLanes,
);
completeUnitOfWork(erroredWork);
} catch (yetAnotherThrownValue) {
// Something in the return path also threw.
thrownValue = yetAnotherThrownValue;
if (workInProgress === erroredWork && erroredWork !== null) {
// If this boundary has already errored, then we had trouble processing
// the error. Bubble it to the next boundary.
erroredWork = erroredWork.return;
workInProgress = erroredWork;
} else {
erroredWork = workInProgress;
}
continue;
}
// Return to the normal work loop.
return;
} while (true);
}
function pushDispatcher() {
const prevDispatcher = ReactCurrentDispatcher.current;
ReactCurrentDispatcher.current = ContextOnlyDispatcher;
if (prevDispatcher === null) {
// The React isomorphic package does not include a default dispatcher.
// Instead the first renderer will lazily attach one, in order to give
// nicer error messages.
return ContextOnlyDispatcher;
} else {
return prevDispatcher;
}
}
function popDispatcher(prevDispatcher) {
ReactCurrentDispatcher.current = prevDispatcher;
}
export function markCommitTimeOfFallback() {
globalMostRecentFallbackTime = now();
}
export function markSkippedUpdateLanes(lane: Lane | Lanes): void {
workInProgressRootSkippedLanes = mergeLanes(
lane,
workInProgressRootSkippedLanes,
);
}
export function renderDidSuspend(): void {
if (workInProgressRootExitStatus === RootIncomplete) {
workInProgressRootExitStatus = RootSuspended;
}
}
export function renderDidSuspendDelayIfPossible(): void {
if (
workInProgressRootExitStatus === RootIncomplete ||
workInProgressRootExitStatus === RootSuspended
) {
workInProgressRootExitStatus = RootSuspendedWithDelay;
}
// Check if there are updates that we skipped tree that might have unblocked
// this render.
if (
workInProgressRoot !== null &&
(includesNonIdleWork(workInProgressRootSkippedLanes) ||
includesNonIdleWork(workInProgressRootUpdatedLanes))
) {
// Mark the current render as suspended so that we switch to working on
// the updates that were skipped. Usually we only suspend at the end of
// the render phase.
// TODO: We should probably always mark the root as suspended immediately
// (inside this function), since by suspending at the end of the render
// phase introduces a potential mistake where we suspend lanes that were
// pinged or updated while we were rendering.
markRootSuspended(workInProgressRoot, workInProgressRootRenderLanes);
}
}
export function renderDidError() {
if (workInProgressRootExitStatus !== RootCompleted) {
workInProgressRootExitStatus = RootErrored;
}
}
// Called during render to determine if anything has suspended.
// Returns false if we're not sure.
export function renderHasNotSuspendedYet(): boolean {
// If something errored or completed, we can't really be sure,
// so those are false.
return workInProgressRootExitStatus === RootIncomplete;
}
function renderRootSync(root: FiberRoot, lanes: Lanes) {
const prevExecutionContext = executionContext;
executionContext |= RenderContext;
const prevDispatcher = pushDispatcher();
// If the root or lanes have changed, throw out the existing stack
// and prepare a fresh one. Otherwise we'll continue where we left off.
if (workInProgressRoot !== root || workInProgressRootRenderLanes !== lanes) {
if (enableUpdaterTracking) {
if (isDevToolsPresent) {
const memoizedUpdaters = root.memoizedUpdaters;
if (memoizedUpdaters.size > 0) {
restorePendingUpdaters(root, workInProgressRootRenderLanes);
memoizedUpdaters.clear();
}
// At this point, move Fibers that scheduled the upcoming work from the Map to the Set.
// If we bailout on this work, we'll move them back (like above).
// It's important to move them now in case the work spawns more work at the same priority with different updaters.
// That way we can keep the current update and future updates separate.
movePendingFibersToMemoized(root, lanes);
}
}
prepareFreshStack(root, lanes);
}
if (__DEV__) {
if (enableDebugTracing) {
logRenderStarted(lanes);
}
}
if (enableSchedulingProfiler) {
markRenderStarted(lanes);
}
do {
try {
workLoopSync();
break;
} catch (thrownValue) {
handleError(root, thrownValue);
}
} while (true);
resetContextDependencies();
executionContext = prevExecutionContext;
popDispatcher(prevDispatcher);
if (workInProgress !== null) {
// This is a sync render, so we should have finished the whole tree.
invariant(
false,
'Cannot commit an incomplete root. This error is likely caused by a ' +
'bug in React. Please file an issue.',
);
}
if (__DEV__) {
if (enableDebugTracing) {
logRenderStopped();
}
}
if (enableSchedulingProfiler) {
markRenderStopped();
}
// Set this to null to indicate there's no in-progress render.
workInProgressRoot = null;
workInProgressRootRenderLanes = NoLanes;
return workInProgressRootExitStatus;
}
// The work loop is an extremely hot path. Tell Closure not to inline it.
/** @noinline */
function workLoopSync() {
// Already timed out, so perform work without checking if we need to yield.
while (workInProgress !== null) {
performUnitOfWork(workInProgress);
}
}
function renderRootConcurrent(root: FiberRoot, lanes: Lanes) {
const prevExecutionContext = executionContext;
executionContext |= RenderContext;
const prevDispatcher = pushDispatcher();
// If the root or lanes have changed, throw out the existing stack
// and prepare a fresh one. Otherwise we'll continue where we left off.
if (workInProgressRoot !== root || workInProgressRootRenderLanes !== lanes) {
if (enableUpdaterTracking) {
if (isDevToolsPresent) {
const memoizedUpdaters = root.memoizedUpdaters;
if (memoizedUpdaters.size > 0) {
restorePendingUpdaters(root, workInProgressRootRenderLanes);
memoizedUpdaters.clear();
}
// At this point, move Fibers that scheduled the upcoming work from the Map to the Set.
// If we bailout on this work, we'll move them back (like above).
// It's important to move them now in case the work spawns more work at the same priority with different updaters.
// That way we can keep the current update and future updates separate.
movePendingFibersToMemoized(root, lanes);
}
}
resetRenderTimer();
prepareFreshStack(root, lanes);
}
if (__DEV__) {
if (enableDebugTracing) {
logRenderStarted(lanes);
}
}
if (enableSchedulingProfiler) {
markRenderStarted(lanes);
}
do {
try {
workLoopConcurrent();
break;
} catch (thrownValue) {
handleError(root, thrownValue);
}
} while (true);
resetContextDependencies();
popDispatcher(prevDispatcher);
executionContext = prevExecutionContext;
if (__DEV__) {
if (enableDebugTracing) {
logRenderStopped();
}
}
// Check if the tree has completed.
if (workInProgress !== null) {
// Still work remaining.
if (enableSchedulingProfiler) {
markRenderYielded();
}
return RootIncomplete;
} else {
// Completed the tree.
if (enableSchedulingProfiler) {
markRenderStopped();
}
// Set this to null to indicate there's no in-progress render.
workInProgressRoot = null;
workInProgressRootRenderLanes = NoLanes;
// Return the final exit status.
return workInProgressRootExitStatus;
}
}
/** @noinline */
function workLoopConcurrent() {
// Perform work until Scheduler asks us to yield
while (workInProgress !== null && !shouldYield()) {
performUnitOfWork(workInProgress);
}
}
function performUnitOfWork(unitOfWork: Fiber): void {
// The current, flushed, state of this fiber is the alternate. Ideally
// nothing should rely on this, but relying on it here means that we don't
// need an additional field on the work in progress.
const current = unitOfWork.alternate;
setCurrentDebugFiberInDEV(unitOfWork);
let next;
if (enableProfilerTimer && (unitOfWork.mode & ProfileMode) !== NoMode) {
startProfilerTimer(unitOfWork);
next = beginWork(current, unitOfWork, subtreeRenderLanes);
stopProfilerTimerIfRunningAndRecordDelta(unitOfWork, true);
} else {
next = beginWork(current, unitOfWork, subtreeRenderLanes);
}
resetCurrentDebugFiberInDEV();
unitOfWork.memoizedProps = unitOfWork.pendingProps;
if (next === null) {
// If this doesn't spawn new work, complete the current work.
completeUnitOfWork(unitOfWork);
} else {
workInProgress = next;
}
ReactCurrentOwner.current = null;
}
function completeUnitOfWork(unitOfWork: Fiber): void {
// Attempt to complete the current unit of work, then move to the next
// sibling. If there are no more siblings, return to the parent fiber.
let completedWork = unitOfWork;
do {
// The current, flushed, state of this fiber is the alternate. Ideally
// nothing should rely on this, but relying on it here means that we don't
// need an additional field on the work in progress.
const current = completedWork.alternate;
const returnFiber = completedWork.return;
// Check if the work completed or if something threw.
if ((completedWork.flags & Incomplete) === NoFlags) {
setCurrentDebugFiberInDEV(completedWork);
let next;
if (
!enableProfilerTimer ||
(completedWork.mode & ProfileMode) === NoMode
) {
next = completeWork(current, completedWork, subtreeRenderLanes);
} else {
startProfilerTimer(completedWork);
next = completeWork(current, completedWork, subtreeRenderLanes);
// Update render duration assuming we didn't error.
stopProfilerTimerIfRunningAndRecordDelta(completedWork, false);
}
resetCurrentDebugFiberInDEV();
if (next !== null) {
// Completing this fiber spawned new work. Work on that next.
workInProgress = next;
return;
}
} else {
// This fiber did not complete because something threw. Pop values off
// the stack without entering the complete phase. If this is a boundary,
// capture values if possible.
const next = unwindWork(completedWork, subtreeRenderLanes);
// Because this fiber did not complete, don't reset its lanes.
if (next !== null) {
// If completing this work spawned new work, do that next. We'll come
// back here again.
// Since we're restarting, remove anything that is not a host effect
// from the effect tag.
next.flags &= HostEffectMask;
workInProgress = next;
return;
}
if (
enableProfilerTimer &&
(completedWork.mode & ProfileMode) !== NoMode
) {
// Record the render duration for the fiber that errored.
stopProfilerTimerIfRunningAndRecordDelta(completedWork, false);
// Include the time spent working on failed children before continuing.
let actualDuration = completedWork.actualDuration;
let child = completedWork.child;
while (child !== null) {
actualDuration += child.actualDuration;
child = child.sibling;
}
completedWork.actualDuration = actualDuration;
}
if (returnFiber !== null) {
// Mark the parent fiber as incomplete and clear its subtree flags.
returnFiber.flags |= Incomplete;
returnFiber.subtreeFlags = NoFlags;
returnFiber.deletions = null;
}
}
const siblingFiber = completedWork.sibling;
if (siblingFiber !== null) {
// If there is more work to do in this returnFiber, do that next.
workInProgress = siblingFiber;
return;
}
// Otherwise, return to the parent
completedWork = returnFiber;
// Update the next thing we're working on in case something throws.
workInProgress = completedWork;
} while (completedWork !== null);
// We've reached the root.
if (workInProgressRootExitStatus === RootIncomplete) {
workInProgressRootExitStatus = RootCompleted;
}
}
function commitRoot(root) {
// TODO: This no longer makes any sense. We already wrap the mutation and
// layout phases. Should be able to remove.
const previousUpdateLanePriority = getCurrentUpdatePriority();
const prevTransition = ReactCurrentBatchConfig.transition;
try {
ReactCurrentBatchConfig.transition = 0;
setCurrentUpdatePriority(DiscreteEventPriority);
commitRootImpl(root, previousUpdateLanePriority);
} finally {
ReactCurrentBatchConfig.transition = prevTransition;
setCurrentUpdatePriority(previousUpdateLanePriority);
}
return null;
}
function commitRootImpl(root, renderPriorityLevel) {
do {
// `flushPassiveEffects` will call `flushSyncUpdateQueue` at the end, which
// means `flushPassiveEffects` will sometimes result in additional
// passive effects. So we need to keep flushing in a loop until there are
// no more pending effects.
// TODO: Might be better if `flushPassiveEffects` did not automatically
// flush synchronous work at the end, to avoid factoring hazards like this.
flushPassiveEffects();
} while (rootWithPendingPassiveEffects !== null);
flushRenderPhaseStrictModeWarningsInDEV();
invariant(
(executionContext & (RenderContext | CommitContext)) === NoContext,
'Should not already be working.',
);
const finishedWork = root.finishedWork;
const lanes = root.finishedLanes;
if (__DEV__) {
if (enableDebugTracing) {
logCommitStarted(lanes);
}
}
if (enableSchedulingProfiler) {
markCommitStarted(lanes);
}
if (finishedWork === null) {
if (__DEV__) {
if (enableDebugTracing) {
logCommitStopped();
}
}
if (enableSchedulingProfiler) {
markCommitStopped();
}
return null;
} else {
if (__DEV__) {
if (lanes === NoLanes) {
console.error(
'root.finishedLanes should not be empty during a commit. This is a ' +
'bug in React.',
);
}
}
}
root.finishedWork = null;
root.finishedLanes = NoLanes;
invariant(
finishedWork !== root.current,
'Cannot commit the same tree as before. This error is likely caused by ' +
'a bug in React. Please file an issue.',
);
// commitRoot never returns a continuation; it always finishes synchronously.
// So we can clear these now to allow a new callback to be scheduled.
root.callbackNode = null;
root.callbackPriority = NoLane;
// Update the first and last pending times on this root. The new first
// pending time is whatever is left on the root fiber.
let remainingLanes = mergeLanes(finishedWork.lanes, finishedWork.childLanes);
markRootFinished(root, remainingLanes);
if (root === workInProgressRoot) {
// We can reset these now that they are finished.
workInProgressRoot = null;
workInProgress = null;
workInProgressRootRenderLanes = NoLanes;
} else {
// This indicates that the last root we worked on is not the same one that
// we're committing now. This most commonly happens when a suspended root
// times out.
}
// If there are pending passive effects, schedule a callback to process them.
// Do this as early as possible, so it is queued before anything else that
// might get scheduled in the commit phase. (See #16714.)
// TODO: Delete all other places that schedule the passive effect callback
// They're redundant.
if (
(finishedWork.subtreeFlags & PassiveMask) !== NoFlags ||
(finishedWork.flags & PassiveMask) !== NoFlags
) {
if (!rootDoesHavePassiveEffects) {
rootDoesHavePassiveEffects = true;
scheduleCallback(NormalSchedulerPriority, () => {
flushPassiveEffects();
return null;
});
}
}
// Check if there are any effects in the whole tree.
// TODO: This is left over from the effect list implementation, where we had
// to check for the existence of `firstEffect` to satisfy Flow. I think the
// only other reason this optimization exists is because it affects profiling.
// Reconsider whether this is necessary.
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 = 0;
const previousPriority = getCurrentUpdatePriority();
setCurrentUpdatePriority(DiscreteEventPriority);
const prevExecutionContext = executionContext;
executionContext |= CommitContext;
// Reset this to null before calling lifecycles
ReactCurrentOwner.current = null;
// The commit phase is broken into several sub-phases. We do a separate pass
// of the effect list for each phase: all mutation effects come before all
// layout effects, and so on.
// The first phase a "before mutation" phase. We use this phase to read the
// state of the host tree right before we mutate it. This is where
// getSnapshotBeforeUpdate is called.
const shouldFireAfterActiveInstanceBlur = commitBeforeMutationEffects(
root,
finishedWork,
);
if (enableProfilerTimer) {
// Mark the current commit time to be shared by all Profilers in this
// batch. This enables them to be grouped later.
recordCommitTime();
}
if (enableProfilerTimer && enableProfilerNestedUpdateScheduledHook) {
// Track the root here, rather than in commitLayoutEffects(), because of ref setters.
// Updates scheduled during ref detachment should also be flagged.
rootCommittingMutationOrLayoutEffects = root;
}
// The next phase is the mutation phase, where we mutate the host tree.
commitMutationEffects(root, finishedWork, lanes);
if (enableCreateEventHandleAPI) {
if (shouldFireAfterActiveInstanceBlur) {
afterActiveInstanceBlur();
}
}
resetAfterCommit(root.containerInfo);
// The work-in-progress tree is now the current tree. This must come after
// the mutation phase, so that the previous tree is still current during
// componentWillUnmount, but before the layout phase, so that the finished
// work is current during componentDidMount/Update.
root.current = finishedWork;
// The next phase is the layout phase, where we call effects that read
// the host tree after it's been mutated. The idiomatic use case for this is
// layout, but class component lifecycles also fire here for legacy reasons.
if (__DEV__) {
if (enableDebugTracing) {
logLayoutEffectsStarted(lanes);
}
}
if (enableSchedulingProfiler) {
markLayoutEffectsStarted(lanes);
}
commitLayoutEffects(finishedWork, root, lanes);
if (__DEV__) {
if (enableDebugTracing) {
logLayoutEffectsStopped();
}
}
if (enableSchedulingProfiler) {
markLayoutEffectsStopped();
}
if (enableProfilerTimer && enableProfilerNestedUpdateScheduledHook) {
rootCommittingMutationOrLayoutEffects = null;
}
// Tell Scheduler to yield at the end of the frame, so the browser has an
// opportunity to paint.
requestPaint();
executionContext = prevExecutionContext;
// Reset the priority to the previous non-sync value.
setCurrentUpdatePriority(previousPriority);
ReactCurrentBatchConfig.transition = prevTransition;
} else {
// No effects.
root.current = finishedWork;
// Measure these anyway so the flamegraph explicitly shows that there were
// no effects.
// TODO: Maybe there's a better way to report this.
if (enableProfilerTimer) {
recordCommitTime();
}
}
const rootDidHavePassiveEffects = rootDoesHavePassiveEffects;
if (rootDoesHavePassiveEffects) {
// This commit has passive effects. Stash a reference to them. But don't
// schedule a callback until after flushing layout work.
rootDoesHavePassiveEffects = false;
rootWithPendingPassiveEffects = root;
pendingPassiveEffectsLanes = lanes;
}
// Read this again, since an effect might have updated it
remainingLanes = root.pendingLanes;
// Check if there's remaining work on this root
if (remainingLanes === NoLanes) {
// If there's no remaining work, we can clear the set of already failed
// error boundaries.
legacyErrorBoundariesThatAlreadyFailed = null;
}
if (__DEV__ && enableStrictEffects) {
if (!rootDidHavePassiveEffects) {
commitDoubleInvokeEffectsInDEV(root.current, false);
}
}
if (includesSomeLane(remainingLanes, (SyncLane: Lane))) {
if (enableProfilerTimer && enableProfilerNestedUpdatePhase) {
markNestedUpdateScheduled();
}
// Count the number of times the root synchronously re-renders without
// finishing. If there are too many, it indicates an infinite update loop.
if (root === rootWithNestedUpdates) {
nestedUpdateCount++;
} else {
nestedUpdateCount = 0;
rootWithNestedUpdates = root;
}
} else {
nestedUpdateCount = 0;
}
onCommitRootDevTools(finishedWork.stateNode, renderPriorityLevel);
if (enableUpdaterTracking) {
if (isDevToolsPresent) {
root.memoizedUpdaters.clear();
}
}
if (__DEV__) {
onCommitRootTestSelector();
}
// Always call this before exiting `commitRoot`, to ensure that any
// additional work on this root is scheduled.
ensureRootIsScheduled(root, now());
if (hasUncaughtError) {
hasUncaughtError = false;
const error = firstUncaughtError;
firstUncaughtError = null;
throw error;
}
// If the passive effects are the result of a discrete render, flush them
// synchronously at the end of the current task so that the result is
// immediately observable. Otherwise, we assume that they are not
// order-dependent and do not need to be observed by external systems, so we
// can wait until after paint.
// TODO: We can optimize this by not scheduling the callback earlier. Since we
// currently schedule the callback in multiple places, will wait until those
// are consolidated.
if (
includesSomeLane(pendingPassiveEffectsLanes, SyncLane) &&
root.tag !== LegacyRoot
) {
flushPassiveEffects();
}
// If layout work was scheduled, flush it now.
flushSyncCallbacks();
if (__DEV__) {
if (enableDebugTracing) {
logCommitStopped();
}
}
if (enableSchedulingProfiler) {
markCommitStopped();
}
return null;
}
export function flushPassiveEffects(): boolean {
// Returns whether passive effects were flushed.
// TODO: Combine this check with the one in flushPassiveEFfectsImpl. We should
// probably just combine the two functions. I believe they were only separate
// in the first place because we used to wrap it with
// `Scheduler.runWithPriority`, which accepts a function. But now we track the
// priority within React itself, so we can mutate the variable directly.
if (rootWithPendingPassiveEffects !== null) {
const renderPriority = lanesToEventPriority(pendingPassiveEffectsLanes);
const priority = lowerEventPriority(DefaultEventPriority, renderPriority);
const prevTransition = ReactCurrentBatchConfig.transition;
const previousPriority = getCurrentUpdatePriority();
try {
ReactCurrentBatchConfig.transition = 0;
setCurrentUpdatePriority(priority);
return flushPassiveEffectsImpl();
} finally {
setCurrentUpdatePriority(previousPriority);
ReactCurrentBatchConfig.transition = prevTransition;
}
}
return false;
}
export function enqueuePendingPassiveProfilerEffect(fiber: Fiber): void {
if (enableProfilerTimer && enableProfilerCommitHooks) {
pendingPassiveProfilerEffects.push(fiber);
if (!rootDoesHavePassiveEffects) {
rootDoesHavePassiveEffects = true;
scheduleCallback(NormalSchedulerPriority, () => {
flushPassiveEffects();
return null;
});
}
}
}
function flushPassiveEffectsImpl() {
if (rootWithPendingPassiveEffects === null) {
return false;
}
const root = rootWithPendingPassiveEffects;
const lanes = pendingPassiveEffectsLanes;
rootWithPendingPassiveEffects = null;
// TODO: This is sometimes out of sync with rootWithPendingPassiveEffects.
// Figure out why and fix it. It's not causing any known issues (probably
// because it's only used for profiling), but it's a refactor hazard.
pendingPassiveEffectsLanes = NoLanes;
invariant(
(executionContext & (RenderContext | CommitContext)) === NoContext,
'Cannot flush passive effects while already rendering.',
);
if (__DEV__) {
if (enableDebugTracing) {
logPassiveEffectsStarted(lanes);
}
}
if (enableSchedulingProfiler) {
markPassiveEffectsStarted(lanes);
}
const prevExecutionContext = executionContext;
executionContext |= CommitContext;
commitPassiveUnmountEffects(root.current);
commitPassiveMountEffects(root, root.current);
// TODO: Move to commitPassiveMountEffects
if (enableProfilerTimer && enableProfilerCommitHooks) {
const profilerEffects = pendingPassiveProfilerEffects;
pendingPassiveProfilerEffects = [];
for (let i = 0; i < profilerEffects.length; i++) {
const fiber = ((profilerEffects[i]: any): Fiber);
commitPassiveEffectDurations(root, fiber);
}
}
if (__DEV__) {
if (enableDebugTracing) {
logPassiveEffectsStopped();
}
}
if (enableSchedulingProfiler) {
markPassiveEffectsStopped();
}
if (__DEV__ && enableStrictEffects) {
commitDoubleInvokeEffectsInDEV(root.current, true);
}
executionContext = prevExecutionContext;
flushSyncCallbacks();
// If additional passive effects were scheduled, increment a counter. If this
// exceeds the limit, we'll fire a warning.
nestedPassiveUpdateCount =
rootWithPendingPassiveEffects === null ? 0 : nestedPassiveUpdateCount + 1;
// TODO: Move to commitPassiveMountEffects
onPostCommitRootDevTools(root);
if (enableProfilerTimer && enableProfilerCommitHooks) {
const stateNode = root.current.stateNode;
stateNode.effectDuration = 0;
stateNode.passiveEffectDuration = 0;
}
return true;
}
export function isAlreadyFailedLegacyErrorBoundary(instance: mixed): boolean {
return (
legacyErrorBoundariesThatAlreadyFailed !== null &&
legacyErrorBoundariesThatAlreadyFailed.has(instance)
);
}
export function markLegacyErrorBoundaryAsFailed(instance: mixed) {
if (legacyErrorBoundariesThatAlreadyFailed === null) {
legacyErrorBoundariesThatAlreadyFailed = new Set([instance]);
} else {
legacyErrorBoundariesThatAlreadyFailed.add(instance);
}
}
function prepareToThrowUncaughtError(error: mixed) {
if (!hasUncaughtError) {
hasUncaughtError = true;
firstUncaughtError = error;
}
}
export const onUncaughtError = prepareToThrowUncaughtError;
function captureCommitPhaseErrorOnRoot(
rootFiber: Fiber,
sourceFiber: Fiber,
error: mixed,
) {
const errorInfo = createCapturedValue(error, sourceFiber);
const update = createRootErrorUpdate(rootFiber, errorInfo, (SyncLane: Lane));
enqueueUpdate(rootFiber, update, (SyncLane: Lane));
const eventTime = requestEventTime();
const root = markUpdateLaneFromFiberToRoot(rootFiber, (SyncLane: Lane));
if (root !== null) {
markRootUpdated(root, SyncLane, eventTime);
ensureRootIsScheduled(root, eventTime);
}
}
export function captureCommitPhaseError(
sourceFiber: Fiber,
nearestMountedAncestor: Fiber | null,
error: mixed,
) {
if (sourceFiber.tag === HostRoot) {
// Error was thrown at the root. There is no parent, so the root
// itself should capture it.
captureCommitPhaseErrorOnRoot(sourceFiber, sourceFiber, error);
return;
}
let fiber = null;
if (skipUnmountedBoundaries) {
fiber = nearestMountedAncestor;
} else {
fiber = sourceFiber.return;
}
while (fiber !== null) {
if (fiber.tag === HostRoot) {
captureCommitPhaseErrorOnRoot(fiber, sourceFiber, error);
return;
} else if (fiber.tag === ClassComponent) {
const ctor = fiber.type;
const instance = fiber.stateNode;
if (
typeof ctor.getDerivedStateFromError === 'function' ||
(typeof instance.componentDidCatch === 'function' &&
!isAlreadyFailedLegacyErrorBoundary(instance))
) {
const errorInfo = createCapturedValue(error, sourceFiber);
const update = createClassErrorUpdate(
fiber,
errorInfo,
(SyncLane: Lane),
);
enqueueUpdate(fiber, update, (SyncLane: Lane));
const eventTime = requestEventTime();
const root = markUpdateLaneFromFiberToRoot(fiber, (SyncLane: Lane));
if (root !== null) {
markRootUpdated(root, SyncLane, eventTime);
ensureRootIsScheduled(root, eventTime);
}
return;
}
}
fiber = fiber.return;
}
if (__DEV__) {
// TODO: Until we re-land skipUnmountedBoundaries (see #20147), this warning
// will fire for errors that are thrown by destroy functions inside deleted
// trees. What it should instead do is propagate the error to the parent of
// the deleted tree. In the meantime, do not add this warning to the
// allowlist; this is only for our internal use.
console.error(
'Internal React error: Attempted to capture a commit phase error ' +
'inside a detached tree. This indicates a bug in React. Likely ' +
'causes include deleting the same fiber more than once, committing an ' +
'already-finished tree, or an inconsistent return pointer.\n\n' +
'Error message:\n\n%s',
error,
);
}
}
export function pingSuspendedRoot(
root: FiberRoot,
wakeable: Wakeable,
pingedLanes: Lanes,
) {
const pingCache = root.pingCache;
if (pingCache !== null) {
// The wakeable resolved, so we no longer need to memoize, because it will
// never be thrown again.
pingCache.delete(wakeable);
}
const eventTime = requestEventTime();
markRootPinged(root, pingedLanes, eventTime);
if (
workInProgressRoot === root &&
isSubsetOfLanes(workInProgressRootRenderLanes, pingedLanes)
) {
// Received a ping at the same priority level at which we're currently
// rendering. We might want to restart this render. This should mirror
// the logic of whether or not a root suspends once it completes.
// TODO: If we're rendering sync either due to Sync, Batched or expired,
// we should probably never restart.
// If we're suspended with delay, or if it's a retry, we'll always suspend
// so we can always restart.
if (
workInProgressRootExitStatus === RootSuspendedWithDelay ||
(workInProgressRootExitStatus === RootSuspended &&
includesOnlyRetries(workInProgressRootRenderLanes) &&
now() - globalMostRecentFallbackTime < FALLBACK_THROTTLE_MS)
) {
// Restart from the root.
prepareFreshStack(root, NoLanes);
} else {
// Even though we can't restart right now, we might get an
// opportunity later. So we mark this render as having a ping.
workInProgressRootPingedLanes = mergeLanes(
workInProgressRootPingedLanes,
pingedLanes,
);
}
}
ensureRootIsScheduled(root, eventTime);
}
function retryTimedOutBoundary(boundaryFiber: Fiber, retryLane: Lane) {
// The boundary fiber (a Suspense component or SuspenseList component)
// previously was rendered in its fallback state. One of the promises that
// suspended it has resolved, which means at least part of the tree was
// likely unblocked. Try rendering again, at a new lanes.
if (retryLane === NoLane) {
// TODO: Assign this to `suspenseState.retryLane`? to avoid
// unnecessary entanglement?
retryLane = requestRetryLane(boundaryFiber);
}
// TODO: Special case idle priority?
const eventTime = requestEventTime();
const root = markUpdateLaneFromFiberToRoot(boundaryFiber, retryLane);
if (root !== null) {
markRootUpdated(root, retryLane, eventTime);
ensureRootIsScheduled(root, eventTime);
}
}
export function retryDehydratedSuspenseBoundary(boundaryFiber: Fiber) {
const suspenseState: null | SuspenseState = boundaryFiber.memoizedState;
let retryLane = NoLane;
if (suspenseState !== null) {
retryLane = suspenseState.retryLane;
}
retryTimedOutBoundary(boundaryFiber, retryLane);
}
export function resolveRetryWakeable(boundaryFiber: Fiber, wakeable: Wakeable) {
let retryLane = NoLane; // Default
let retryCache: WeakSet<Wakeable> | Set<Wakeable> | null;
if (enableSuspenseServerRenderer) {
switch (boundaryFiber.tag) {
case SuspenseComponent:
retryCache = boundaryFiber.stateNode;
const suspenseState: null | SuspenseState = boundaryFiber.memoizedState;
if (suspenseState !== null) {
retryLane = suspenseState.retryLane;
}
break;
case SuspenseListComponent:
retryCache = boundaryFiber.stateNode;
break;
default:
invariant(
false,
'Pinged unknown suspense boundary type. ' +
'This is probably a bug in React.',
);
}
} else {
retryCache = boundaryFiber.stateNode;
}
if (retryCache !== null) {
// The wakeable resolved, so we no longer need to memoize, because it will
// never be thrown again.
retryCache.delete(wakeable);
}
retryTimedOutBoundary(boundaryFiber, retryLane);
}
// Computes the next Just Noticeable Difference (JND) boundary.
// The theory is that a person can't tell the difference between small differences in time.
// Therefore, if we wait a bit longer than necessary that won't translate to a noticeable
// difference in the experience. However, waiting for longer might mean that we can avoid
// showing an intermediate loading state. The longer we have already waited, the harder it
// is to tell small differences in time. Therefore, the longer we've already waited,
// the longer we can wait additionally. At some point we have to give up though.
// We pick a train model where the next boundary commits at a consistent schedule.
// These particular numbers are vague estimates. We expect to adjust them based on research.
function jnd(timeElapsed: number) {
return timeElapsed < 120
? 120
: timeElapsed < 480
? 480
: timeElapsed < 1080
? 1080
: timeElapsed < 1920
? 1920
: timeElapsed < 3000
? 3000
: timeElapsed < 4320
? 4320
: ceil(timeElapsed / 1960) * 1960;
}
function checkForNestedUpdates() {
if (nestedUpdateCount > NESTED_UPDATE_LIMIT) {
nestedUpdateCount = 0;
rootWithNestedUpdates = null;
invariant(
false,
'Maximum update depth exceeded. This can happen when a component ' +
'repeatedly calls setState inside componentWillUpdate or ' +
'componentDidUpdate. React limits the number of nested updates to ' +
'prevent infinite loops.',
);
}
if (__DEV__) {
if (nestedPassiveUpdateCount > NESTED_PASSIVE_UPDATE_LIMIT) {
nestedPassiveUpdateCount = 0;
console.error(
'Maximum update depth exceeded. This can happen when a component ' +
"calls setState inside useEffect, but useEffect either doesn't " +
'have a dependency array, or one of the dependencies changes on ' +
'every render.',
);
}
}
}
function flushRenderPhaseStrictModeWarningsInDEV() {
if (__DEV__) {
ReactStrictModeWarnings.flushLegacyContextWarning();
if (warnAboutDeprecatedLifecycles) {
ReactStrictModeWarnings.flushPendingUnsafeLifecycleWarnings();
}
}
}
function commitDoubleInvokeEffectsInDEV(
fiber: Fiber,
hasPassiveEffects: boolean,
) {
if (__DEV__ && enableStrictEffects) {
// TODO (StrictEffects) Should we set a marker on the root if it contains strict effects
// so we don't traverse unnecessarily? similar to subtreeFlags but just at the root level.
// Maybe not a big deal since this is DEV only behavior.
setCurrentDebugFiberInDEV(fiber);
invokeEffectsInDev(fiber, MountLayoutDev, invokeLayoutEffectUnmountInDEV);
if (hasPassiveEffects) {
invokeEffectsInDev(
fiber,
MountPassiveDev,
invokePassiveEffectUnmountInDEV,
);
}
invokeEffectsInDev(fiber, MountLayoutDev, invokeLayoutEffectMountInDEV);
if (hasPassiveEffects) {
invokeEffectsInDev(fiber, MountPassiveDev, invokePassiveEffectMountInDEV);
}
resetCurrentDebugFiberInDEV();
}
}
function invokeEffectsInDev(
firstChild: Fiber,
fiberFlags: Flags,
invokeEffectFn: (fiber: Fiber) => void,
): void {
if (__DEV__ && enableStrictEffects) {
// We don't need to re-check StrictEffectsMode here.
// This function is only called if that check has already passed.
let current = firstChild;
let subtreeRoot = null;
while (current !== null) {
const primarySubtreeFlag = current.subtreeFlags & fiberFlags;
if (
current !== subtreeRoot &&
current.child !== null &&
primarySubtreeFlag !== NoFlags
) {
current = current.child;
} else {
if ((current.flags & fiberFlags) !== NoFlags) {
invokeEffectFn(current);
}
if (current.sibling !== null) {
current = current.sibling;
} else {
current = subtreeRoot = current.return;
}
}
}
}
}
let didWarnStateUpdateForNotYetMountedComponent: Set<string> | null = null;
function warnAboutUpdateOnNotYetMountedFiberInDEV(fiber) {
if (__DEV__) {
if ((executionContext & RenderContext) !== NoContext) {
// We let the other warning about render phase updates deal with this one.
return;
}
if (!(fiber.mode & ConcurrentMode)) {
return;
}
const tag = fiber.tag;
if (
tag !== IndeterminateComponent &&
tag !== HostRoot &&
tag !== ClassComponent &&
tag !== FunctionComponent &&
tag !== ForwardRef &&
tag !== MemoComponent &&
tag !== SimpleMemoComponent
) {
// Only warn for user-defined components, not internal ones like Suspense.
return;
}
// We show the whole stack but dedupe on the top component's name because
// the problematic code almost always lies inside that component.
const componentName = getComponentNameFromFiber(fiber) || 'ReactComponent';
if (didWarnStateUpdateForNotYetMountedComponent !== null) {
if (didWarnStateUpdateForNotYetMountedComponent.has(componentName)) {
return;
}
didWarnStateUpdateForNotYetMountedComponent.add(componentName);
} else {
didWarnStateUpdateForNotYetMountedComponent = new Set([componentName]);
}
const previousFiber = ReactCurrentFiberCurrent;
try {
setCurrentDebugFiberInDEV(fiber);
console.error(
"Can't perform a React state update on a component that hasn't mounted yet. " +
'This indicates that you have a side-effect in your render function that ' +
'asynchronously later calls tries to update the component. Move this work to ' +
'useEffect instead.',
);
} finally {
if (previousFiber) {
setCurrentDebugFiberInDEV(fiber);
} else {
resetCurrentDebugFiberInDEV();
}
}
}
}
let beginWork;
if (__DEV__ && replayFailedUnitOfWorkWithInvokeGuardedCallback) {
const dummyFiber = null;
beginWork = (current, unitOfWork, lanes) => {
// If a component throws an error, we replay it again in a synchronously
// dispatched event, so that the debugger will treat it as an uncaught
// error See ReactErrorUtils for more information.
// Before entering the begin phase, copy the work-in-progress onto a dummy
// fiber. If beginWork throws, we'll use this to reset the state.
const originalWorkInProgressCopy = assignFiberPropertiesInDEV(
dummyFiber,
unitOfWork,
);
try {
return originalBeginWork(current, unitOfWork, lanes);
} catch (originalError) {
if (
originalError !== null &&
typeof originalError === 'object' &&
typeof originalError.then === 'function'
) {
// Don't replay promises. Treat everything else like an error.
throw originalError;
}
// Keep this code in sync with handleError; any changes here must have
// corresponding changes there.
resetContextDependencies();
resetHooksAfterThrow();
// Don't reset current debug fiber, since we're about to work on the
// same fiber again.
// Unwind the failed stack frame
unwindInterruptedWork(unitOfWork, workInProgressRootRenderLanes);
// Restore the original properties of the fiber.
assignFiberPropertiesInDEV(unitOfWork, originalWorkInProgressCopy);
if (enableProfilerTimer && unitOfWork.mode & ProfileMode) {
// Reset the profiler timer.
startProfilerTimer(unitOfWork);
}
// Run beginWork again.
invokeGuardedCallback(
null,
originalBeginWork,
null,
current,
unitOfWork,
lanes,
);
if (hasCaughtError()) {
const replayError = clearCaughtError();
if (
typeof replayError === 'object' &&
replayError !== null &&
replayError._suppressLogging &&
typeof originalError === 'object' &&
originalError !== null &&
!originalError._suppressLogging
) {
// If suppressed, let the flag carry over to the original error which is the one we'll rethrow.
originalError._suppressLogging = true;
}
}
// We always throw the original error in case the second render pass is not idempotent.
// This can happen if a memoized function or CommonJS module doesn't throw after first invokation.
throw originalError;
}
};
} else {
beginWork = originalBeginWork;
}
let didWarnAboutUpdateInRender = false;
let didWarnAboutUpdateInRenderForAnotherComponent;
if (__DEV__) {
didWarnAboutUpdateInRenderForAnotherComponent = new Set();
}
function warnAboutRenderPhaseUpdatesInDEV(fiber) {
if (__DEV__) {
if (
ReactCurrentDebugFiberIsRenderingInDEV &&
(executionContext & RenderContext) !== NoContext &&
!getIsUpdatingOpaqueValueInRenderPhaseInDEV()
) {
switch (fiber.tag) {
case FunctionComponent:
case ForwardRef:
case SimpleMemoComponent: {
const renderingComponentName =
(workInProgress && getComponentNameFromFiber(workInProgress)) ||
'Unknown';
// Dedupe by the rendering component because it's the one that needs to be fixed.
const dedupeKey = renderingComponentName;
if (!didWarnAboutUpdateInRenderForAnotherComponent.has(dedupeKey)) {
didWarnAboutUpdateInRenderForAnotherComponent.add(dedupeKey);
const setStateComponentName =
getComponentNameFromFiber(fiber) || 'Unknown';
console.error(
'Cannot update a component (`%s`) while rendering a ' +
'different component (`%s`). To locate the bad setState() call inside `%s`, ' +
'follow the stack trace as described in https://reactjs.org/link/setstate-in-render',
setStateComponentName,
renderingComponentName,
renderingComponentName,
);
}
break;
}
case ClassComponent: {
if (!didWarnAboutUpdateInRender) {
console.error(
'Cannot update during an existing state transition (such as ' +
'within `render`). Render methods should be a pure ' +
'function of props and state.',
);
didWarnAboutUpdateInRender = true;
}
break;
}
}
}
}
}
export function restorePendingUpdaters(root: FiberRoot, lanes: Lanes): void {
if (enableUpdaterTracking) {
if (isDevToolsPresent) {
const memoizedUpdaters = root.memoizedUpdaters;
memoizedUpdaters.forEach(schedulingFiber => {
addFiberToLanesMap(root, schedulingFiber, lanes);
});
// This function intentionally does not clear memoized updaters.
// Those may still be relevant to the current commit
// and a future one (e.g. Suspense).
}
}
}
const fakeActCallbackNode = {};
function scheduleCallback(priorityLevel, callback) {
if (__DEV__) {
// If we're currently inside an `act` scope, bypass Scheduler and push to
// the `act` queue instead.
const actQueue = ReactCurrentActQueue.current;
if (actQueue !== null) {
actQueue.push(callback);
return fakeActCallbackNode;
} else {
return Scheduler_scheduleCallback(priorityLevel, callback);
}
} else {
// In production, always call Scheduler. This function will be stripped out.
return Scheduler_scheduleCallback(priorityLevel, callback);
}
}
function cancelCallback(callbackNode) {
if (__DEV__ && callbackNode === fakeActCallbackNode) {
return;
}
// In production, always call Scheduler. This function will be stripped out.
return Scheduler_cancelCallback(callbackNode);
}
function shouldForceFlushFallbacksInDEV() {
// Never force flush in production. This function should get stripped out.
return __DEV__ && ReactCurrentActQueue.current !== null;
}
export function warnIfNotCurrentlyActingEffectsInDEV(fiber: Fiber): void {
if (__DEV__) {
if (
warnsIfNotActing === true &&
(fiber.mode & StrictLegacyMode) !== NoMode &&
ReactCurrentActQueue.current === null &&
// Our internal tests use a custom implementation of `act` that works by
// mocking the Scheduler package. Disable the `act` warning.
// TODO: Maybe the warning should be disabled by default, and then turned
// on at the testing frameworks layer? Instead of what we do now, which
// is check if a `jest` global is defined.
ReactCurrentActQueue.disableActWarning === false
) {
console.error(
'An update to %s ran an effect, but was not wrapped in act(...).\n\n' +
'When testing, code that causes React state updates should be ' +
'wrapped into act(...):\n\n' +
'act(() => {\n' +
' /* fire events that update state */\n' +
'});\n' +
'/* assert on the output */\n\n' +
"This ensures that you're testing the behavior the user would see " +
'in the browser.' +
' Learn more at https://reactjs.org/link/wrap-tests-with-act',
getComponentNameFromFiber(fiber),
);
}
}
}
function warnIfNotCurrentlyActingUpdatesInDEV(fiber: Fiber): void {
if (__DEV__) {
if (
warnsIfNotActing === true &&
executionContext === NoContext &&
ReactCurrentActQueue.current === null &&
// Our internal tests use a custom implementation of `act` that works by
// mocking the Scheduler package. Disable the `act` warning.
// TODO: Maybe the warning should be disabled by default, and then turned
// on at the testing frameworks layer? Instead of what we do now, which
// is check if a `jest` global is defined.
ReactCurrentActQueue.disableActWarning === false
) {
const previousFiber = ReactCurrentFiberCurrent;