Files
react/packages/react-reconciler/src/ReactFiberWorkLoop.old.js
T
2020-08-13 15:11:01 +01:00

3855 lines
128 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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 {Thenable, Wakeable} from 'shared/ReactTypes';
import type {Fiber, FiberRoot} from './ReactInternalTypes';
import type {Lanes, Lane} from './ReactFiberLane';
import type {ReactPriorityLevel} from './ReactInternalTypes';
import type {Interaction} from 'scheduler/src/Tracing';
import type {SuspenseConfig} from './ReactFiberSuspenseConfig';
import type {SuspenseState} from './ReactFiberSuspenseComponent.old';
import type {Effect as HookEffect} from './ReactFiberHooks.old';
import type {StackCursor} from './ReactFiberStack.old';
import {
warnAboutDeprecatedLifecycles,
enableSuspenseServerRenderer,
replayFailedUnitOfWorkWithInvokeGuardedCallback,
enableProfilerTimer,
enableProfilerCommitHooks,
enableSchedulerTracing,
warnAboutUnmockedScheduler,
deferRenderPhaseUpdateToNextBatch,
decoupleUpdatePriorityFromScheduler,
enableDebugTracing,
enableSchedulingProfiler,
enableScopeAPI,
} from 'shared/ReactFeatureFlags';
import ReactSharedInternals from 'shared/ReactSharedInternals';
import invariant from 'shared/invariant';
import {
scheduleCallback,
cancelCallback,
getCurrentPriorityLevel,
runWithPriority,
shouldYield,
requestPaint,
now,
NoPriority as NoSchedulerPriority,
ImmediatePriority as ImmediateSchedulerPriority,
UserBlockingPriority as UserBlockingSchedulerPriority,
NormalPriority as NormalSchedulerPriority,
flushSyncCallbackQueue,
scheduleSyncCallback,
} from './SchedulerWithReactIntegration.old';
import {
logCommitStarted,
logCommitStopped,
logLayoutEffectsStarted,
logLayoutEffectsStopped,
logPassiveEffectsStarted,
logPassiveEffectsStopped,
logRenderStarted,
logRenderStopped,
} from './DebugTracing';
import {
markCommitStarted,
markCommitStopped,
markLayoutEffectsStarted,
markLayoutEffectsStopped,
markPassiveEffectsStarted,
markPassiveEffectsStopped,
markRenderStarted,
markRenderYielded,
markRenderStopped,
} from './SchedulingProfiler';
// The scheduler is imported here *only* to detect whether it's been mocked
import * as Scheduler from 'scheduler';
import {__interactionsRef, __subscriberRef} from 'scheduler/tracing';
import {
prepareForCommit,
resetAfterCommit,
scheduleTimeout,
cancelTimeout,
noTimeout,
warnsIfNotActing,
beforeActiveInstanceBlur,
afterActiveInstanceBlur,
clearContainer,
} from './ReactFiberHostConfig';
import {
createWorkInProgress,
assignFiberPropertiesInDEV,
} from './ReactFiber.old';
import {
NoMode,
StrictMode,
ProfileMode,
BlockingMode,
ConcurrentMode,
} from './ReactTypeOfMode';
import {
HostRoot,
IndeterminateComponent,
ClassComponent,
SuspenseComponent,
SuspenseListComponent,
FunctionComponent,
ForwardRef,
MemoComponent,
SimpleMemoComponent,
Block,
OffscreenComponent,
LegacyHiddenComponent,
ScopeComponent,
} from './ReactWorkTags';
import {LegacyRoot} from './ReactRootTags';
import {
NoEffect,
PerformedWork,
Placement,
Update,
PlacementAndUpdate,
Deletion,
Ref,
ContentReset,
Snapshot,
Callback,
Passive,
PassiveUnmountPendingDev,
Incomplete,
HostEffectMask,
Hydrating,
HydratingAndUpdate,
} from './ReactSideEffectTags';
import {
NoLanePriority,
SyncLanePriority,
SyncBatchedLanePriority,
InputDiscreteLanePriority,
TransitionShortLanePriority,
TransitionLongLanePriority,
DefaultLanePriority,
NoLanes,
NoLane,
SyncLane,
SyncBatchedLane,
OffscreenLane,
NoTimestamp,
findUpdateLane,
findTransitionLane,
findRetryLane,
includesSomeLane,
isSubsetOfLanes,
mergeLanes,
removeLanes,
pickArbitraryLane,
hasDiscreteLanes,
includesNonIdleWork,
includesOnlyRetries,
getNextLanes,
returnNextLanesPriority,
setCurrentUpdateLanePriority,
getCurrentUpdateLanePriority,
markStarvedLanesAsExpired,
getLanesToRetrySynchronouslyOnError,
getMostRecentEventTime,
markRootUpdated,
markRootSuspended as markRootSuspended_dontCallThisOneDirectly,
markRootPinged,
markRootExpired,
markDiscreteUpdatesExpired,
markRootFinished,
schedulerPriorityToLanePriority,
lanePriorityToSchedulerPriority,
} from './ReactFiberLane';
import {beginWork as originalBeginWork} from './ReactFiberBeginWork.old';
import {completeWork} from './ReactFiberCompleteWork.old';
import {unwindWork, unwindInterruptedWork} from './ReactFiberUnwindWork.old';
import {
throwException,
createRootErrorUpdate,
createClassErrorUpdate,
} from './ReactFiberThrow.old';
import {
commitBeforeMutationLifeCycles as commitBeforeMutationEffectOnFiber,
commitLifeCycles as commitLayoutEffectOnFiber,
commitPlacement,
commitWork,
commitDeletion,
commitDetachRef,
commitAttachRef,
commitPassiveEffectDurations,
commitResetTextContent,
isSuspenseBoundaryBeingHidden,
} from './ReactFiberCommitWork.old';
import {enqueueUpdate} from './ReactUpdateQueue.old';
import {resetContextDependencies} from './ReactFiberNewContext.old';
import {
resetHooksAfterThrow,
ContextOnlyDispatcher,
getIsUpdatingOpaqueValueInRenderPhaseInDEV,
} from './ReactFiberHooks.old';
import {createCapturedValue} from './ReactCapturedValue';
import {
push as pushToStack,
pop as popFromStack,
createCursor,
} from './ReactFiberStack.old';
import {
recordCommitTime,
recordPassiveEffectDuration,
startPassiveEffectTimer,
startProfilerTimer,
stopProfilerTimerIfRunningAndRecordDelta,
} from './ReactProfilerTimer.old';
// DEV stuff
import getComponentName from 'shared/getComponentName';
import ReactStrictModeWarnings from './ReactStrictModeWarnings.old';
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} from './ReactFiberDevToolsHook.old';
import {onCommitRoot as onCommitRootTestSelector} from './ReactTestSelectors';
// Used by `act`
import enqueueTask from 'shared/enqueueTask';
import {doesFiberContain} from './ReactFiberTreeReflection';
const ceil = Math.ceil;
const {
ReactCurrentDispatcher,
ReactCurrentOwner,
IsSomeRendererActing,
} = ReactSharedInternals;
type ExecutionContext = number;
export const NoContext = /* */ 0b0000000;
const BatchedContext = /* */ 0b0000001;
const EventContext = /* */ 0b0000010;
const DiscreteEventContext = /* */ 0b0000100;
const LegacyUnbatchedContext = /* */ 0b0001000;
const RenderContext = /* */ 0b0010000;
const CommitContext = /* */ 0b0100000;
export const RetryAfterError = /* */ 0b1000000;
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.
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;
let workInProgressRootLatestSuspenseTimeout: number = NoTimestamp;
let workInProgressRootCanSuspendUsingConfig: null | SuspenseConfig = 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;
let mostRecentlyUpdatedRoot: FiberRoot | null = null;
// 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;
const DEFAULT_TIMEOUT_MS: number = 5000;
let nextEffect: Fiber | null = null;
let hasUncaughtError = false;
let firstUncaughtError = null;
let legacyErrorBoundariesThatAlreadyFailed: Set<mixed> | null = null;
let rootDoesHavePassiveEffects: boolean = false;
let rootWithPendingPassiveEffects: FiberRoot | null = null;
let pendingPassiveEffectsRenderPriority: ReactPriorityLevel = NoSchedulerPriority;
let pendingPassiveEffectsLanes: Lanes = NoLanes;
let pendingPassiveHookEffectsMount: Array<HookEffect | Fiber> = [];
let pendingPassiveHookEffectsUnmount: Array<HookEffect | Fiber> = [];
let pendingPassiveProfilerEffects: Array<Fiber> = [];
let rootsWithPendingDiscreteUpdates: Set<FiberRoot> | null = null;
// 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;
// Marks the need to reschedule pending interactions at these lanes
// during the commit phase. This enables them to be traced across components
// that spawn new work during render. E.g. hidden boundaries, suspended SSR
// hydration or SuspenseList.
// TODO: Can use a bitmask instead of an array
let spawnedWorkDuringRender: null | Array<Lane | Lanes> = null;
// 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 currentEventWipLanes: Lanes = NoLanes;
let currentEventPendingLanes: Lanes = NoLanes;
// Dev only flag that tracks if passive effects are currently being flushed.
// We warn about state updates for unmounted components differently in this case.
let isFlushingPassiveEffects = false;
let focusedInstanceHandle: null | Fiber = null;
let shouldFireAfterActiveInstanceBlur: boolean = false;
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,
suspenseConfig: SuspenseConfig | null,
): Lane {
// Special cases
const mode = fiber.mode;
if ((mode & BlockingMode) === NoMode) {
return (SyncLane: Lane);
} else if ((mode & ConcurrentMode) === NoMode) {
return getCurrentPriorityLevel() === ImmediateSchedulerPriority
? (SyncLane: Lane)
: (SyncBatchedLane: 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" (expiration time) 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);
}
// 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. For example, we use the `renderLanes`
// to avoid choosing a lane that is already in the middle of rendering.
//
// However, the "included" lanes could be mutated in between updates in the
// same event, like if you perform an update inside `flushSync`. Or any other
// code path that might call `prepareFreshStack`.
//
// 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.
//
// We'll do the same for `currentEventPendingLanes` below.
if (currentEventWipLanes === NoLanes) {
currentEventWipLanes = workInProgressRootIncludedLanes;
}
if (suspenseConfig !== null) {
// Use the size of the timeout as a heuristic to prioritize shorter
// transitions over longer ones.
// TODO: This will coerce numbers larger than 31 bits to 0.
const timeoutMs = suspenseConfig.timeoutMs;
const transitionLanePriority =
timeoutMs === undefined || (timeoutMs | 0) < 10000
? TransitionShortLanePriority
: TransitionLongLanePriority;
if (currentEventPendingLanes !== NoLanes) {
currentEventPendingLanes =
mostRecentlyUpdatedRoot !== null
? mostRecentlyUpdatedRoot.pendingLanes
: NoLanes;
}
return findTransitionLane(
transitionLanePriority,
currentEventWipLanes,
currentEventPendingLanes,
);
}
// TODO: Remove this dependency on the Scheduler priority.
// To do that, we're replacing it with an update lane priority.
const schedulerPriority = getCurrentPriorityLevel();
// The old behavior was using the priority level of the Scheduler.
// This couples React to the Scheduler internals, so we're replacing it
// with the currentUpdateLanePriority above. As an example of how this
// could be problematic, if we're not inside `Scheduler.runWithPriority`,
// then we'll get the priority of the current running Scheduler task,
// which is probably not what we want.
let lane;
if (
// TODO: Temporary. We're removing the concept of discrete updates.
(executionContext & DiscreteEventContext) !== NoContext &&
schedulerPriority === UserBlockingSchedulerPriority
) {
lane = findUpdateLane(InputDiscreteLanePriority, currentEventWipLanes);
} else {
const schedulerLanePriority = schedulerPriorityToLanePriority(
schedulerPriority,
);
if (decoupleUpdatePriorityFromScheduler) {
// In the new strategy, we will track the current update lane priority
// inside React and use that priority to select a lane for this update.
// For now, we're just logging when they're different so we can assess.
const currentUpdateLanePriority = getCurrentUpdateLanePriority();
if (
schedulerLanePriority !== currentUpdateLanePriority &&
currentUpdateLanePriority !== NoLanePriority
) {
if (__DEV__) {
console.error(
'Expected current scheduler lane priority %s to match current update lane priority %s',
schedulerLanePriority,
currentUpdateLanePriority,
);
}
}
}
lane = findUpdateLane(schedulerLanePriority, currentEventWipLanes);
}
return lane;
}
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 & BlockingMode) === NoMode) {
return (SyncLane: Lane);
} else if ((mode & ConcurrentMode) === NoMode) {
return getCurrentPriorityLevel() === ImmediateSchedulerPriority
? (SyncLane: Lane)
: (SyncBatchedLane: Lane);
}
// See `requestUpdateLane` for explanation of `currentEventWipLanes`
if (currentEventWipLanes === NoLanes) {
currentEventWipLanes = workInProgressRootIncludedLanes;
}
return findRetryLane(currentEventWipLanes);
}
export function scheduleUpdateOnFiber(
fiber: Fiber,
lane: Lane,
eventTime: number,
) {
checkForNestedUpdates();
warnAboutRenderPhaseUpdatesInDEV(fiber);
const root = markUpdateLaneFromFiberToRoot(fiber, lane);
if (root === null) {
warnAboutUpdateOnUnmountedFiberInDEV(fiber);
return null;
}
// Mark that the root has a pending update.
markRootUpdated(root, lane, eventTime);
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);
}
}
// TODO: requestUpdateLanePriority also reads the priority. Pass the
// priority as an argument to that function and this one.
const priorityLevel = getCurrentPriorityLevel();
if (lane === SyncLane) {
if (
// Check if we're inside unbatchedUpdates
(executionContext & LegacyUnbatchedContext) !== NoContext &&
// Check if we're not already rendering
(executionContext & (RenderContext | CommitContext)) === NoContext
) {
// Register pending interactions on the root to avoid losing traced interaction data.
schedulePendingInteractions(root, lane);
// This is a legacy edge case. The initial mount of a ReactDOM.render-ed
// root inside of batchedUpdates should be synchronous, but layout updates
// should be deferred until the end of the batch.
performSyncWorkOnRoot(root);
} else {
ensureRootIsScheduled(root, eventTime);
schedulePendingInteractions(root, lane);
if (executionContext === NoContext) {
// 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.
flushSyncCallbackQueue();
}
}
} else {
// Schedule a discrete update but only if it's not Sync.
if (
(executionContext & DiscreteEventContext) !== NoContext &&
// Only updates at user-blocking priority or greater are considered
// discrete, even inside a discrete event.
(priorityLevel === UserBlockingSchedulerPriority ||
priorityLevel === ImmediateSchedulerPriority)
) {
// This is the result of a discrete event. Track the lowest priority
// discrete update per root so we can flush them early, if needed.
if (rootsWithPendingDiscreteUpdates === null) {
rootsWithPendingDiscreteUpdates = new Set([root]);
} else {
rootsWithPendingDiscreteUpdates.add(root);
}
}
// Schedule other updates after in case the callback is sync.
ensureRootIsScheduled(root, eventTime);
schedulePendingInteractions(root, lane);
}
// We use this when assigning a lane for a transition inside
// `requestUpdateLane`. We assume it's the same as the root being updated,
// since in the common case of a single root app it probably is. If it's not
// the same root, then it's not a huge deal, we just might batch more stuff
// together more than necessary.
mostRecentlyUpdatedRoot = 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.effectTag & (Placement | Hydrating)) !== NoEffect
) {
warnAboutUpdateOnNotYetMountedFiberInDEV(sourceFiber);
}
}
// Walk the parent path to the root and update the child expiration time.
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.effectTag & (Placement | Hydrating)) !== NoEffect) {
warnAboutUpdateOnNotYetMountedFiberInDEV(sourceFiber);
}
}
}
node = parent;
parent = parent.return;
}
if (node.tag === HostRoot) {
const root: FiberRoot = node.stateNode;
return root;
} else {
return null;
}
}
// 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,
);
// This returns the priority level computed during the `getNextLanes` call.
const newCallbackPriority = returnNextLanesPriority();
if (nextLanes === NoLanes) {
// Special case: There's nothing to work on.
if (existingCallbackNode !== null) {
cancelCallback(existingCallbackNode);
root.callbackNode = null;
root.callbackPriority = NoLanePriority;
}
return;
}
// Check if there's an existing task. We may be able to reuse it.
if (existingCallbackNode !== null) {
const existingCallbackPriority = root.callbackPriority;
if (existingCallbackPriority === newCallbackPriority) {
// The priority hasn't changed. We can reuse the existing task. Exit.
return;
}
// The priority changed. Cancel the existing callback. We'll schedule a new
// one below.
cancelCallback(existingCallbackNode);
}
// Schedule a new callback.
let newCallbackNode;
if (newCallbackPriority === SyncLanePriority) {
// Special case: Sync React callbacks are scheduled on a special
// internal queue
newCallbackNode = scheduleSyncCallback(
performSyncWorkOnRoot.bind(null, root),
);
} else if (newCallbackPriority === SyncBatchedLanePriority) {
newCallbackNode = scheduleCallback(
ImmediateSchedulerPriority,
performSyncWorkOnRoot.bind(null, root),
);
} else {
const schedulerPriorityLevel = lanePriorityToSchedulerPriority(
newCallbackPriority,
);
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) {
// 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;
currentEventWipLanes = NoLanes;
currentEventPendingLanes = 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 expiration time 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;
}
// TODO: We only check `didTimeout` defensively, to account for a Scheduler
// bug where `shouldYield` sometimes returns `true` even if `didTimeout` is
// true, which leads to an infinite loop. Once the bug in Scheduler is
// fixed, we can remove this, since we track expiration ourselves.
if (didTimeout) {
// Something expired. Flush synchronously until there's no expired
// work left.
markRootExpired(root, lanes);
// This will schedule a synchronous callback.
ensureRootIsScheduled(root, now());
return null;
}
let exitStatus = renderRootConcurrent(root, lanes);
if (
includesSomeLane(
workInProgressRootIncludedLanes,
workInProgressRootUpdatedLanes,
)
) {
// The render included lanes that were updated during the render phase.
// For example, when unhiding a hidden tree, we include all the lanes
// that were previously skipped when the tree was hidden. That set of
// lanes is a superset of the lanes we started rendering with.
//
// So we'll throw out the current work and restart.
prepareFreshStack(root, NoLanes);
} else if (exitStatus !== RootIncomplete) {
if (exitStatus === RootErrored) {
executionContext |= RetryAfterError;
// If an error occurred during hydration,
// discard server response and fall back to client side render.
if (root.hydrate) {
root.hydrate = false;
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.
lanes = getLanesToRetrySynchronouslyOnError(root);
if (lanes !== NoLanes) {
exitStatus = renderRootSync(root, lanes);
}
}
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, finishedWork, 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, finishedWork, 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 (
// do not delay if we're inside an act() scope
!shouldForceFlushFallbacksInDEV()
) {
// We're suspended in a state that should be avoided. We'll try to
// avoid committing it for as long as the timeouts let us.
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;
}
const mostRecentEventTime = getMostRecentEventTime(root, lanes);
let msUntilTimeout;
if (workInProgressRootLatestSuspenseTimeout !== NoTimestamp) {
// We have processed a suspense config whose expiration time we
// can use as the timeout.
msUntilTimeout = workInProgressRootLatestSuspenseTimeout - now();
} else if (mostRecentEventTime === NoTimestamp) {
// This should never normally happen because only new updates
// cause delayed states, so we should have processed something.
// However, this could also happen in an offscreen tree.
msUntilTimeout = 0;
} else {
// If we didn't process a suspense config, compute a JND based on
// the amount of time elapsed since the most recent event time.
const eventTimeMs = mostRecentEventTime;
const timeElapsedMs = now() - eventTimeMs;
msUntilTimeout = jnd(timeElapsedMs) - timeElapsedMs;
}
// Don't bother with a very short suspense time.
if (msUntilTimeout > 10) {
// 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 RootCompleted: {
// The work completed. Ready to commit.
const mostRecentEventTime = getMostRecentEventTime(root, lanes);
if (
// do not delay if we're inside an act() scope
!shouldForceFlushFallbacksInDEV() &&
mostRecentEventTime !== NoTimestamp &&
workInProgressRootCanSuspendUsingConfig !== null
) {
// If we have exceeded the minimum loading delay, which probably
// means we have shown a spinner already, we might have to suspend
// a bit longer to ensure that the spinner is shown for
// enough time.
const msUntilTimeout = computeMsUntilSuspenseLoadingDelay(
mostRecentEventTime,
workInProgressRootCanSuspendUsingConfig,
);
if (msUntilTimeout > 10) {
markRootSuspended(root, lanes);
root.timeoutHandle = scheduleTimeout(
commitRoot.bind(null, root),
msUntilTimeout,
);
break;
}
}
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) {
invariant(
(executionContext & (RenderContext | CommitContext)) === NoContext,
'Should not already be working.',
);
flushPassiveEffects();
let lanes;
let exitStatus;
if (
root === workInProgressRoot &&
includesSomeLane(root.expiredLanes, workInProgressRootRenderLanes)
) {
// There's a partial tree, and at least one of its lanes has expired. Finish
// rendering it before rendering the rest of the expired work.
lanes = workInProgressRootRenderLanes;
exitStatus = renderRootSync(root, lanes);
if (
includesSomeLane(
workInProgressRootIncludedLanes,
workInProgressRootUpdatedLanes,
)
) {
// The render included lanes that were updated during the render phase.
// For example, when unhiding a hidden tree, we include all the lanes
// that were previously skipped when the tree was hidden. That set of
// lanes is a superset of the lanes we started rendering with.
//
// Note that this only happens when part of the tree is rendered
// concurrently. If the whole tree is rendered synchronously, then there
// are no interleaved events.
lanes = getNextLanes(root, lanes);
exitStatus = renderRootSync(root, lanes);
}
} else {
lanes = getNextLanes(root, NoLanes);
exitStatus = renderRootSync(root, lanes);
}
if (root.tag !== LegacyRoot && exitStatus === RootErrored) {
executionContext |= RetryAfterError;
// If an error occurred during hydration,
// discard server response and fall back to client side render.
if (root.hydrate) {
root.hydrate = false;
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.
lanes = getLanesToRetrySynchronouslyOnError(root);
if (lanes !== NoLanes) {
exitStatus = renderRootSync(root, lanes);
}
}
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) {
markRootExpired(root, lanes);
ensureRootIsScheduled(root, now());
if ((executionContext & (RenderContext | CommitContext)) === NoContext) {
flushSyncCallbackQueue();
}
}
export function getExecutionContext(): ExecutionContext {
return executionContext;
}
export function flushDiscreteUpdates() {
// TODO: Should be able to flush inside batchedUpdates, but not inside `act`.
// However, `act` uses `batchedUpdates`, so there's no way to distinguish
// those two cases. Need to fix this before exposing flushDiscreteUpdates
// as a public API.
if (
(executionContext & (BatchedContext | RenderContext | CommitContext)) !==
NoContext
) {
if (__DEV__) {
if ((executionContext & RenderContext) !== NoContext) {
console.error(
'unstable_flushDiscreteUpdates: Cannot flush updates when React is ' +
'already rendering.',
);
}
}
// We're already rendering, so we can't synchronously flush pending work.
// This is probably a nested event dispatch triggered by a lifecycle/effect,
// like `el.focus()`. Exit.
return;
}
flushPendingDiscreteUpdates();
// If the discrete updates scheduled passive effects, flush them now so that
// they fire before the next serial event.
flushPassiveEffects();
}
export function deferredUpdates<A>(fn: () => A): A {
if (decoupleUpdatePriorityFromScheduler) {
const previousLanePriority = getCurrentUpdateLanePriority();
try {
setCurrentUpdateLanePriority(DefaultLanePriority);
return runWithPriority(NormalSchedulerPriority, fn);
} finally {
setCurrentUpdateLanePriority(previousLanePriority);
}
} else {
return runWithPriority(NormalSchedulerPriority, fn);
}
}
function flushPendingDiscreteUpdates() {
if (rootsWithPendingDiscreteUpdates !== null) {
// For each root with pending discrete updates, schedule a callback to
// immediately flush them.
const roots = rootsWithPendingDiscreteUpdates;
rootsWithPendingDiscreteUpdates = null;
roots.forEach(root => {
markDiscreteUpdatesExpired(root);
ensureRootIsScheduled(root, now());
});
}
// Now flush the immediate queue.
flushSyncCallbackQueue();
}
export function batchedUpdates<A, R>(fn: A => R, a: A): R {
const prevExecutionContext = executionContext;
executionContext |= BatchedContext;
try {
return fn(a);
} finally {
executionContext = prevExecutionContext;
if (executionContext === NoContext) {
// Flush the immediate callbacks that were scheduled during this batch
flushSyncCallbackQueue();
}
}
}
export function batchedEventUpdates<A, R>(fn: A => R, a: A): R {
const prevExecutionContext = executionContext;
executionContext |= EventContext;
try {
return fn(a);
} finally {
executionContext = prevExecutionContext;
if (executionContext === NoContext) {
// Flush the immediate callbacks that were scheduled during this batch
flushSyncCallbackQueue();
}
}
}
export function discreteUpdates<A, B, C, D, R>(
fn: (A, B, C) => R,
a: A,
b: B,
c: C,
d: D,
): R {
const prevExecutionContext = executionContext;
executionContext |= DiscreteEventContext;
if (decoupleUpdatePriorityFromScheduler) {
const previousLanePriority = getCurrentUpdateLanePriority();
try {
setCurrentUpdateLanePriority(InputDiscreteLanePriority);
return runWithPriority(
UserBlockingSchedulerPriority,
fn.bind(null, a, b, c, d),
);
} finally {
setCurrentUpdateLanePriority(previousLanePriority);
executionContext = prevExecutionContext;
if (executionContext === NoContext) {
// Flush the immediate callbacks that were scheduled during this batch
flushSyncCallbackQueue();
}
}
} else {
try {
return runWithPriority(
UserBlockingSchedulerPriority,
fn.bind(null, a, b, c, d),
);
} finally {
executionContext = prevExecutionContext;
if (executionContext === NoContext) {
// Flush the immediate callbacks that were scheduled during this batch
flushSyncCallbackQueue();
}
}
}
}
export function unbatchedUpdates<A, R>(fn: (a: A) => R, a: A): R {
const prevExecutionContext = executionContext;
executionContext &= ~BatchedContext;
executionContext |= LegacyUnbatchedContext;
try {
return fn(a);
} finally {
executionContext = prevExecutionContext;
if (executionContext === NoContext) {
// Flush the immediate callbacks that were scheduled during this batch
flushSyncCallbackQueue();
}
}
}
export function flushSync<A, R>(fn: A => R, a: A): R {
const prevExecutionContext = executionContext;
if ((prevExecutionContext & (RenderContext | CommitContext)) !== NoContext) {
if (__DEV__) {
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 fn(a);
}
executionContext |= BatchedContext;
if (decoupleUpdatePriorityFromScheduler) {
const previousLanePriority = getCurrentUpdateLanePriority();
try {
setCurrentUpdateLanePriority(SyncLanePriority);
if (fn) {
return runWithPriority(ImmediateSchedulerPriority, fn.bind(null, a));
} else {
return (undefined: $FlowFixMe);
}
} finally {
setCurrentUpdateLanePriority(previousLanePriority);
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.
flushSyncCallbackQueue();
}
} else {
try {
if (fn) {
return runWithPriority(ImmediateSchedulerPriority, fn.bind(null, a));
} else {
return (undefined: $FlowFixMe);
}
} finally {
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.
flushSyncCallbackQueue();
}
}
}
export function flushControlled(fn: () => mixed): void {
const prevExecutionContext = executionContext;
executionContext |= BatchedContext;
if (decoupleUpdatePriorityFromScheduler) {
const previousLanePriority = getCurrentUpdateLanePriority();
try {
setCurrentUpdateLanePriority(SyncLanePriority);
runWithPriority(ImmediateSchedulerPriority, fn);
} finally {
setCurrentUpdateLanePriority(previousLanePriority);
executionContext = prevExecutionContext;
if (executionContext === NoContext) {
// Flush the immediate callbacks that were scheduled during this batch
flushSyncCallbackQueue();
}
}
} else {
try {
runWithPriority(ImmediateSchedulerPriority, fn);
} finally {
executionContext = prevExecutionContext;
if (executionContext === NoContext) {
// Flush the immediate callbacks that were scheduled during this batch
flushSyncCallbackQueue();
}
}
}
}
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);
interruptedWork = interruptedWork.return;
}
}
workInProgressRoot = root;
workInProgress = createWorkInProgress(root.current, null);
workInProgressRootRenderLanes = subtreeRenderLanes = workInProgressRootIncludedLanes = lanes;
workInProgressRootExitStatus = RootIncomplete;
workInProgressRootFatalError = null;
workInProgressRootLatestSuspenseTimeout = NoTimestamp;
workInProgressRootCanSuspendUsingConfig = null;
workInProgressRootSkippedLanes = NoLanes;
workInProgressRootUpdatedLanes = NoLanes;
workInProgressRootPingedLanes = NoLanes;
if (enableSchedulerTracing) {
spawnedWorkDuringRender = null;
}
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;
}
function pushInteractions(root) {
if (enableSchedulerTracing) {
const prevInteractions: Set<Interaction> | null = __interactionsRef.current;
__interactionsRef.current = root.memoizedInteractions;
return prevInteractions;
}
return null;
}
function popInteractions(prevInteractions) {
if (enableSchedulerTracing) {
__interactionsRef.current = prevInteractions;
}
}
export function markCommitTimeOfFallback() {
globalMostRecentFallbackTime = now();
}
export function markRenderEventTimeAndConfig(
eventTime: number,
suspenseConfig: null | SuspenseConfig,
): void {
// Track the largest/latest timeout deadline in this batch.
// TODO: If there are two transitions in the same batch, shouldn't we
// choose the smaller one? Maybe this is because when an intermediate
// transition is superseded, we should ignore its suspense config, but
// we don't currently.
if (suspenseConfig !== null) {
// If `timeoutMs` is not specified, we default to 5 seconds. We have to
// resolve this default here because `suspenseConfig` is owned
// by userspace.
// TODO: Store this on the root instead (transition -> timeoutMs)
// TODO: Should this default to a JND instead?
const timeoutMs = suspenseConfig.timeoutMs | 0 || DEFAULT_TIMEOUT_MS;
const timeoutTime = eventTime + timeoutMs;
if (timeoutTime > workInProgressRootLatestSuspenseTimeout) {
workInProgressRootLatestSuspenseTimeout = timeoutTime;
workInProgressRootCanSuspendUsingConfig = suspenseConfig;
}
}
}
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) {
prepareFreshStack(root, lanes);
startWorkOnPendingInteractions(root, lanes);
}
const prevInteractions = pushInteractions(root);
if (__DEV__) {
if (enableDebugTracing) {
logRenderStarted(lanes);
}
}
if (enableSchedulingProfiler) {
markRenderStarted(lanes);
}
do {
try {
workLoopSync();
break;
} catch (thrownValue) {
handleError(root, thrownValue);
}
} while (true);
resetContextDependencies();
if (enableSchedulerTracing) {
popInteractions(((prevInteractions: any): Set<Interaction>));
}
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) {
prepareFreshStack(root, lanes);
startWorkOnPendingInteractions(root, lanes);
}
const prevInteractions = pushInteractions(root);
if (__DEV__) {
if (enableDebugTracing) {
logRenderStarted(lanes);
}
}
if (enableSchedulingProfiler) {
markRenderStarted(lanes);
}
do {
try {
workLoopConcurrent();
break;
} catch (thrownValue) {
handleError(root, thrownValue);
}
} while (true);
resetContextDependencies();
if (enableSchedulerTracing) {
popInteractions(((prevInteractions: any): Set<Interaction>));
}
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.effectTag & Incomplete) === NoEffect) {
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;
}
resetChildLanes(completedWork);
if (
returnFiber !== null &&
// Do not append effects to parents if a sibling failed to complete
(returnFiber.effectTag & Incomplete) === NoEffect
) {
// Append all the effects of the subtree and this fiber onto the effect
// list of the parent. The completion order of the children affects the
// side-effect order.
if (returnFiber.firstEffect === null) {
returnFiber.firstEffect = completedWork.firstEffect;
}
if (completedWork.lastEffect !== null) {
if (returnFiber.lastEffect !== null) {
returnFiber.lastEffect.nextEffect = completedWork.firstEffect;
}
returnFiber.lastEffect = completedWork.lastEffect;
}
// If this fiber had side-effects, we append it AFTER the children's
// side-effects. We can perform certain side-effects earlier if needed,
// by doing multiple passes over the effect list. We don't want to
// schedule our own side-effect on our own list because if end up
// reusing children we'll schedule this effect onto itself since we're
// at the end.
const effectTag = completedWork.effectTag;
// Skip both NoWork and PerformedWork tags when creating the effect
// list. PerformedWork effect is read by React DevTools but shouldn't be
// committed.
if (effectTag > PerformedWork) {
if (returnFiber.lastEffect !== null) {
returnFiber.lastEffect.nextEffect = completedWork;
} else {
returnFiber.firstEffect = completedWork;
}
returnFiber.lastEffect = completedWork;
}
}
} 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 expiration time.
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.effectTag &= 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 effect list.
returnFiber.firstEffect = returnFiber.lastEffect = null;
returnFiber.effectTag |= Incomplete;
}
}
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 resetChildLanes(completedWork: Fiber) {
if (
// TODO: Move this check out of the hot path by moving `resetChildLanes`
// to switch statement in `completeWork`.
(completedWork.tag === LegacyHiddenComponent ||
completedWork.tag === OffscreenComponent) &&
completedWork.memoizedState !== null &&
!includesSomeLane(subtreeRenderLanes, (OffscreenLane: Lane)) &&
(completedWork.mode & ConcurrentMode) !== NoLanes
) {
// The children of this component are hidden. Don't bubble their
// expiration times.
return;
}
let newChildLanes = NoLanes;
// Bubble up the earliest expiration time.
if (enableProfilerTimer && (completedWork.mode & ProfileMode) !== NoMode) {
// In profiling mode, resetChildExpirationTime is also used to reset
// profiler durations.
let actualDuration = completedWork.actualDuration;
let treeBaseDuration = ((completedWork.selfBaseDuration: any): number);
// When a fiber is cloned, its actualDuration is reset to 0. This value will
// only be updated if work is done on the fiber (i.e. it doesn't bailout).
// When work is done, it should bubble to the parent's actualDuration. If
// the fiber has not been cloned though, (meaning no work was done), then
// this value will reflect the amount of time spent working on a previous
// render. In that case it should not bubble. We determine whether it was
// cloned by comparing the child pointer.
const shouldBubbleActualDurations =
completedWork.alternate === null ||
completedWork.child !== completedWork.alternate.child;
let child = completedWork.child;
while (child !== null) {
newChildLanes = mergeLanes(
newChildLanes,
mergeLanes(child.lanes, child.childLanes),
);
if (shouldBubbleActualDurations) {
actualDuration += child.actualDuration;
}
treeBaseDuration += child.treeBaseDuration;
child = child.sibling;
}
const isTimedOutSuspense =
completedWork.tag === SuspenseComponent &&
completedWork.memoizedState !== null;
if (isTimedOutSuspense) {
// Don't count time spent in a timed out Suspense subtree as part of the base duration.
const primaryChildFragment = completedWork.child;
if (primaryChildFragment !== null) {
treeBaseDuration -= ((primaryChildFragment.treeBaseDuration: any): number);
}
}
completedWork.actualDuration = actualDuration;
completedWork.treeBaseDuration = treeBaseDuration;
} else {
let child = completedWork.child;
while (child !== null) {
newChildLanes = mergeLanes(
newChildLanes,
mergeLanes(child.lanes, child.childLanes),
);
child = child.sibling;
}
}
completedWork.childLanes = newChildLanes;
}
function commitRoot(root) {
const renderPriorityLevel = getCurrentPriorityLevel();
runWithPriority(
ImmediateSchedulerPriority,
commitRootImpl.bind(null, root, renderPriorityLevel),
);
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;
}
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;
// 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);
// Clear already finished discrete updates in case that a later call of
// `flushDiscreteUpdates` starts a useless render pass which may cancels
// a scheduled timeout.
if (rootsWithPendingDiscreteUpdates !== null) {
if (
!hasDiscreteLanes(remainingLanes) &&
rootsWithPendingDiscreteUpdates.has(root)
) {
rootsWithPendingDiscreteUpdates.delete(root);
}
}
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.
}
// Get the list of effects.
let firstEffect;
if (finishedWork.effectTag > PerformedWork) {
// A fiber's effect list consists only of its children, not itself. So if
// the root has an effect, we need to add it to the end of the list. The
// resulting list is the set that would belong to the root's parent, if it
// had one; that is, all the effects in the tree including the root.
if (finishedWork.lastEffect !== null) {
finishedWork.lastEffect.nextEffect = finishedWork;
firstEffect = finishedWork.firstEffect;
} else {
firstEffect = finishedWork;
}
} else {
// There is no effect on the root.
firstEffect = finishedWork.firstEffect;
}
if (firstEffect !== null) {
let previousLanePriority;
if (decoupleUpdatePriorityFromScheduler) {
previousLanePriority = getCurrentUpdateLanePriority();
setCurrentUpdateLanePriority(SyncLanePriority);
}
const prevExecutionContext = executionContext;
executionContext |= CommitContext;
const prevInteractions = pushInteractions(root);
// 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.
focusedInstanceHandle = prepareForCommit(root.containerInfo);
shouldFireAfterActiveInstanceBlur = false;
nextEffect = firstEffect;
do {
if (__DEV__) {
invokeGuardedCallback(null, commitBeforeMutationEffects, null);
if (hasCaughtError()) {
invariant(nextEffect !== null, 'Should be working on an effect.');
const error = clearCaughtError();
captureCommitPhaseError(nextEffect, error);
nextEffect = nextEffect.nextEffect;
}
} else {
try {
commitBeforeMutationEffects();
} catch (error) {
invariant(nextEffect !== null, 'Should be working on an effect.');
captureCommitPhaseError(nextEffect, error);
nextEffect = nextEffect.nextEffect;
}
}
} while (nextEffect !== null);
// We no longer need to track the active instance fiber
focusedInstanceHandle = null;
if (enableProfilerTimer) {
// Mark the current commit time to be shared by all Profilers in this
// batch. This enables them to be grouped later.
recordCommitTime();
}
// The next phase is the mutation phase, where we mutate the host tree.
nextEffect = firstEffect;
do {
if (__DEV__) {
invokeGuardedCallback(
null,
commitMutationEffects,
null,
root,
renderPriorityLevel,
);
if (hasCaughtError()) {
invariant(nextEffect !== null, 'Should be working on an effect.');
const error = clearCaughtError();
captureCommitPhaseError(nextEffect, error);
nextEffect = nextEffect.nextEffect;
}
} else {
try {
commitMutationEffects(root, renderPriorityLevel);
} catch (error) {
invariant(nextEffect !== null, 'Should be working on an effect.');
captureCommitPhaseError(nextEffect, error);
nextEffect = nextEffect.nextEffect;
}
}
} while (nextEffect !== null);
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.
nextEffect = firstEffect;
do {
if (__DEV__) {
invokeGuardedCallback(null, commitLayoutEffects, null, root, lanes);
if (hasCaughtError()) {
invariant(nextEffect !== null, 'Should be working on an effect.');
const error = clearCaughtError();
captureCommitPhaseError(nextEffect, error);
nextEffect = nextEffect.nextEffect;
}
} else {
try {
commitLayoutEffects(root, lanes);
} catch (error) {
invariant(nextEffect !== null, 'Should be working on an effect.');
captureCommitPhaseError(nextEffect, error);
nextEffect = nextEffect.nextEffect;
}
}
} while (nextEffect !== null);
nextEffect = null;
// Tell Scheduler to yield at the end of the frame, so the browser has an
// opportunity to paint.
requestPaint();
if (enableSchedulerTracing) {
popInteractions(((prevInteractions: any): Set<Interaction>));
}
executionContext = prevExecutionContext;
if (decoupleUpdatePriorityFromScheduler && previousLanePriority != null) {
// Reset the priority to the previous non-sync value.
setCurrentUpdateLanePriority(previousLanePriority);
}
} 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;
pendingPassiveEffectsRenderPriority = renderPriorityLevel;
} else {
// We are done with the effect chain at this point so let's clear the
// nextEffect pointers to assist with GC. If we have passive effects, we'll
// clear this in flushPassiveEffects.
nextEffect = firstEffect;
while (nextEffect !== null) {
const nextNextEffect = nextEffect.nextEffect;
nextEffect.nextEffect = null;
if (nextEffect.effectTag & Deletion) {
detachFiberAfterEffects(nextEffect);
}
nextEffect = nextNextEffect;
}
}
// 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 (enableSchedulerTracing) {
if (spawnedWorkDuringRender !== null) {
const expirationTimes = spawnedWorkDuringRender;
spawnedWorkDuringRender = null;
for (let i = 0; i < expirationTimes.length; i++) {
scheduleInteractions(
root,
expirationTimes[i],
root.memoizedInteractions,
);
}
}
schedulePendingInteractions(root, remainingLanes);
}
} else {
// If there's no remaining work, we can clear the set of already failed
// error boundaries.
legacyErrorBoundariesThatAlreadyFailed = null;
}
if (enableSchedulerTracing) {
if (!rootDidHavePassiveEffects) {
// If there are no passive effects, then we can complete the pending interactions.
// Otherwise, we'll wait until after the passive effects are flushed.
// Wait to do this until after remaining work has been scheduled,
// so that we don't prematurely signal complete for interactions when there's e.g. hidden work.
finishPendingInteractions(root, lanes);
}
}
if (remainingLanes === SyncLane) {
// 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 (__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 ((executionContext & LegacyUnbatchedContext) !== NoContext) {
if (__DEV__) {
if (enableDebugTracing) {
logCommitStopped();
}
}
if (enableSchedulingProfiler) {
markCommitStopped();
}
// This is a legacy edge case. We just committed the initial mount of
// a ReactDOM.render-ed root inside of batchedUpdates. The commit fired
// synchronously, but layout updates should be deferred until the end
// of the batch.
return null;
}
// If layout work was scheduled, flush it now.
flushSyncCallbackQueue();
if (__DEV__) {
if (enableDebugTracing) {
logCommitStopped();
}
}
if (enableSchedulingProfiler) {
markCommitStopped();
}
return null;
}
function commitBeforeMutationEffects() {
while (nextEffect !== null) {
const current = nextEffect.alternate;
if (!shouldFireAfterActiveInstanceBlur && focusedInstanceHandle !== null) {
if ((nextEffect.effectTag & Deletion) !== NoEffect) {
if (doesFiberContain(nextEffect, focusedInstanceHandle)) {
shouldFireAfterActiveInstanceBlur = true;
beforeActiveInstanceBlur();
}
} else {
// TODO: Move this out of the hot path using a dedicated effect tag.
if (
nextEffect.tag === SuspenseComponent &&
isSuspenseBoundaryBeingHidden(current, nextEffect) &&
doesFiberContain(nextEffect, focusedInstanceHandle)
) {
shouldFireAfterActiveInstanceBlur = true;
beforeActiveInstanceBlur();
}
}
}
const effectTag = nextEffect.effectTag;
if ((effectTag & Snapshot) !== NoEffect) {
setCurrentDebugFiberInDEV(nextEffect);
commitBeforeMutationEffectOnFiber(current, nextEffect);
resetCurrentDebugFiberInDEV();
}
if ((effectTag & Passive) !== NoEffect) {
// If there are passive effects, schedule a callback to flush at
// the earliest opportunity.
if (!rootDoesHavePassiveEffects) {
rootDoesHavePassiveEffects = true;
scheduleCallback(NormalSchedulerPriority, () => {
flushPassiveEffects();
return null;
});
}
}
nextEffect = nextEffect.nextEffect;
}
}
function commitMutationEffects(root: FiberRoot, renderPriorityLevel) {
// TODO: Should probably move the bulk of this function to commitWork.
while (nextEffect !== null) {
setCurrentDebugFiberInDEV(nextEffect);
const effectTag = nextEffect.effectTag;
if (effectTag & ContentReset) {
commitResetTextContent(nextEffect);
}
if (effectTag & Ref) {
const current = nextEffect.alternate;
if (current !== null) {
commitDetachRef(current);
}
if (enableScopeAPI) {
// TODO: This is a temporary solution that allowed us to transition away
// from React Flare on www.
if (nextEffect.tag === ScopeComponent) {
commitAttachRef(nextEffect);
}
}
}
// The following switch statement is only concerned about placement,
// updates, and deletions. To avoid needing to add a case for every possible
// bitmap value, we remove the secondary effects from the effect tag and
// switch on that value.
const primaryEffectTag =
effectTag & (Placement | Update | Deletion | Hydrating);
switch (primaryEffectTag) {
case Placement: {
commitPlacement(nextEffect);
// Clear the "placement" from effect tag so that we know that this is
// inserted, before any life-cycles like componentDidMount gets called.
// TODO: findDOMNode doesn't rely on this any more but isMounted does
// and isMounted is deprecated anyway so we should be able to kill this.
nextEffect.effectTag &= ~Placement;
break;
}
case PlacementAndUpdate: {
// Placement
commitPlacement(nextEffect);
// Clear the "placement" from effect tag so that we know that this is
// inserted, before any life-cycles like componentDidMount gets called.
nextEffect.effectTag &= ~Placement;
// Update
const current = nextEffect.alternate;
commitWork(current, nextEffect);
break;
}
case Hydrating: {
nextEffect.effectTag &= ~Hydrating;
break;
}
case HydratingAndUpdate: {
nextEffect.effectTag &= ~Hydrating;
// Update
const current = nextEffect.alternate;
commitWork(current, nextEffect);
break;
}
case Update: {
const current = nextEffect.alternate;
commitWork(current, nextEffect);
break;
}
case Deletion: {
commitDeletion(root, nextEffect, renderPriorityLevel);
break;
}
}
resetCurrentDebugFiberInDEV();
nextEffect = nextEffect.nextEffect;
}
}
function commitLayoutEffects(root: FiberRoot, committedLanes: Lanes) {
if (__DEV__) {
if (enableDebugTracing) {
logLayoutEffectsStarted(committedLanes);
}
}
if (enableSchedulingProfiler) {
markLayoutEffectsStarted(committedLanes);
}
// TODO: Should probably move the bulk of this function to commitWork.
while (nextEffect !== null) {
setCurrentDebugFiberInDEV(nextEffect);
const effectTag = nextEffect.effectTag;
if (effectTag & (Update | Callback)) {
const current = nextEffect.alternate;
commitLayoutEffectOnFiber(root, current, nextEffect, committedLanes);
}
if (enableScopeAPI) {
// TODO: This is a temporary solution that allowed us to transition away
// from React Flare on www.
if (effectTag & Ref && nextEffect.tag !== ScopeComponent) {
commitAttachRef(nextEffect);
}
} else {
if (effectTag & Ref) {
commitAttachRef(nextEffect);
}
}
resetCurrentDebugFiberInDEV();
nextEffect = nextEffect.nextEffect;
}
if (__DEV__) {
if (enableDebugTracing) {
logLayoutEffectsStopped();
}
}
if (enableSchedulingProfiler) {
markLayoutEffectsStopped();
}
}
export function flushPassiveEffects(): boolean {
// Returns whether passive effects were flushed.
if (pendingPassiveEffectsRenderPriority !== NoSchedulerPriority) {
const priorityLevel =
pendingPassiveEffectsRenderPriority > NormalSchedulerPriority
? NormalSchedulerPriority
: pendingPassiveEffectsRenderPriority;
pendingPassiveEffectsRenderPriority = NoSchedulerPriority;
if (decoupleUpdatePriorityFromScheduler) {
const previousLanePriority = getCurrentUpdateLanePriority();
try {
setCurrentUpdateLanePriority(
schedulerPriorityToLanePriority(priorityLevel),
);
return runWithPriority(priorityLevel, flushPassiveEffectsImpl);
} finally {
setCurrentUpdateLanePriority(previousLanePriority);
}
} else {
return runWithPriority(priorityLevel, flushPassiveEffectsImpl);
}
}
return false;
}
export function enqueuePendingPassiveProfilerEffect(fiber: Fiber): void {
if (enableProfilerTimer && enableProfilerCommitHooks) {
pendingPassiveProfilerEffects.push(fiber);
if (!rootDoesHavePassiveEffects) {
rootDoesHavePassiveEffects = true;
scheduleCallback(NormalSchedulerPriority, () => {
flushPassiveEffects();
return null;
});
}
}
}
export function enqueuePendingPassiveHookEffectMount(
fiber: Fiber,
effect: HookEffect,
): void {
pendingPassiveHookEffectsMount.push(effect, fiber);
if (!rootDoesHavePassiveEffects) {
rootDoesHavePassiveEffects = true;
scheduleCallback(NormalSchedulerPriority, () => {
flushPassiveEffects();
return null;
});
}
}
export function enqueuePendingPassiveHookEffectUnmount(
fiber: Fiber,
effect: HookEffect,
): void {
pendingPassiveHookEffectsUnmount.push(effect, fiber);
if (__DEV__) {
fiber.effectTag |= PassiveUnmountPendingDev;
const alternate = fiber.alternate;
if (alternate !== null) {
alternate.effectTag |= PassiveUnmountPendingDev;
}
}
if (!rootDoesHavePassiveEffects) {
rootDoesHavePassiveEffects = true;
scheduleCallback(NormalSchedulerPriority, () => {
flushPassiveEffects();
return null;
});
}
}
function invokePassiveEffectCreate(effect: HookEffect): void {
const create = effect.create;
effect.destroy = create();
}
function flushPassiveEffectsImpl() {
if (rootWithPendingPassiveEffects === null) {
return false;
}
const root = rootWithPendingPassiveEffects;
const lanes = pendingPassiveEffectsLanes;
rootWithPendingPassiveEffects = null;
pendingPassiveEffectsLanes = NoLanes;
invariant(
(executionContext & (RenderContext | CommitContext)) === NoContext,
'Cannot flush passive effects while already rendering.',
);
if (__DEV__) {
if (enableDebugTracing) {
logPassiveEffectsStarted(lanes);
}
}
if (enableSchedulingProfiler) {
markPassiveEffectsStarted(lanes);
}
if (__DEV__) {
isFlushingPassiveEffects = true;
}
const prevExecutionContext = executionContext;
executionContext |= CommitContext;
const prevInteractions = pushInteractions(root);
// It's important that ALL pending passive effect destroy functions are called
// before ANY passive effect create functions are called.
// Otherwise effects in sibling components might interfere with each other.
// e.g. a destroy function in one component may unintentionally override a ref
// value set by a create function in another component.
// Layout effects have the same constraint.
// First pass: Destroy stale passive effects.
const unmountEffects = pendingPassiveHookEffectsUnmount;
pendingPassiveHookEffectsUnmount = [];
for (let i = 0; i < unmountEffects.length; i += 2) {
const effect = ((unmountEffects[i]: any): HookEffect);
const fiber = ((unmountEffects[i + 1]: any): Fiber);
const destroy = effect.destroy;
effect.destroy = undefined;
if (__DEV__) {
fiber.effectTag &= ~PassiveUnmountPendingDev;
const alternate = fiber.alternate;
if (alternate !== null) {
alternate.effectTag &= ~PassiveUnmountPendingDev;
}
}
if (typeof destroy === 'function') {
if (__DEV__) {
setCurrentDebugFiberInDEV(fiber);
if (
enableProfilerTimer &&
enableProfilerCommitHooks &&
fiber.mode & ProfileMode
) {
startPassiveEffectTimer();
invokeGuardedCallback(null, destroy, null);
recordPassiveEffectDuration(fiber);
} else {
invokeGuardedCallback(null, destroy, null);
}
if (hasCaughtError()) {
invariant(fiber !== null, 'Should be working on an effect.');
const error = clearCaughtError();
captureCommitPhaseError(fiber, error);
}
resetCurrentDebugFiberInDEV();
} else {
try {
if (
enableProfilerTimer &&
enableProfilerCommitHooks &&
fiber.mode & ProfileMode
) {
try {
startPassiveEffectTimer();
destroy();
} finally {
recordPassiveEffectDuration(fiber);
}
} else {
destroy();
}
} catch (error) {
invariant(fiber !== null, 'Should be working on an effect.');
captureCommitPhaseError(fiber, error);
}
}
}
}
// Second pass: Create new passive effects.
const mountEffects = pendingPassiveHookEffectsMount;
pendingPassiveHookEffectsMount = [];
for (let i = 0; i < mountEffects.length; i += 2) {
const effect = ((mountEffects[i]: any): HookEffect);
const fiber = ((mountEffects[i + 1]: any): Fiber);
if (__DEV__) {
setCurrentDebugFiberInDEV(fiber);
if (
enableProfilerTimer &&
enableProfilerCommitHooks &&
fiber.mode & ProfileMode
) {
startPassiveEffectTimer();
invokeGuardedCallback(null, invokePassiveEffectCreate, null, effect);
recordPassiveEffectDuration(fiber);
} else {
invokeGuardedCallback(null, invokePassiveEffectCreate, null, effect);
}
if (hasCaughtError()) {
invariant(fiber !== null, 'Should be working on an effect.');
const error = clearCaughtError();
captureCommitPhaseError(fiber, error);
}
resetCurrentDebugFiberInDEV();
} else {
try {
const create = effect.create;
if (
enableProfilerTimer &&
enableProfilerCommitHooks &&
fiber.mode & ProfileMode
) {
try {
startPassiveEffectTimer();
effect.destroy = create();
} finally {
recordPassiveEffectDuration(fiber);
}
} else {
effect.destroy = create();
}
} catch (error) {
invariant(fiber !== null, 'Should be working on an effect.');
captureCommitPhaseError(fiber, error);
}
}
}
// Note: This currently assumes there are no passive effects on the root fiber
// because the root is not part of its own effect list.
// This could change in the future.
let effect = root.current.firstEffect;
while (effect !== null) {
const nextNextEffect = effect.nextEffect;
// Remove nextEffect pointer to assist GC
effect.nextEffect = null;
if (effect.effectTag & Deletion) {
detachFiberAfterEffects(effect);
}
effect = nextNextEffect;
}
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 (enableSchedulerTracing) {
popInteractions(((prevInteractions: any): Set<Interaction>));
finishPendingInteractions(root, lanes);
}
if (__DEV__) {
isFlushingPassiveEffects = false;
}
if (__DEV__) {
if (enableDebugTracing) {
logPassiveEffectsStopped();
}
}
if (enableSchedulingProfiler) {
markPassiveEffectsStopped();
}
executionContext = prevExecutionContext;
flushSyncCallbackQueue();
// 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;
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);
const eventTime = requestEventTime();
const root = markUpdateLaneFromFiberToRoot(rootFiber, (SyncLane: Lane));
if (root !== null) {
markRootUpdated(root, SyncLane, eventTime);
ensureRootIsScheduled(root, eventTime);
schedulePendingInteractions(root, SyncLane);
}
}
export function captureCommitPhaseError(sourceFiber: Fiber, 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 = 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);
const eventTime = requestEventTime();
const root = markUpdateLaneFromFiberToRoot(fiber, (SyncLane: Lane));
if (root !== null) {
markRootUpdated(root, SyncLane, eventTime);
ensureRootIsScheduled(root, eventTime);
schedulePendingInteractions(root, SyncLane);
}
return;
}
}
fiber = fiber.return;
}
}
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);
schedulePendingInteractions(root, pingedLanes);
}
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 expiration time.
if (retryLane === NoLane) {
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);
schedulePendingInteractions(root, retryLane);
}
}
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 computeMsUntilSuspenseLoadingDelay(
mostRecentEventTime: number,
suspenseConfig: SuspenseConfig,
) {
const busyMinDurationMs = (suspenseConfig.busyMinDurationMs: any) | 0;
if (busyMinDurationMs <= 0) {
return 0;
}
const busyDelayMs = (suspenseConfig.busyDelayMs: any) | 0;
// Compute the time until this render pass would expire.
const currentTimeMs: number = now();
const eventTimeMs: number = mostRecentEventTime;
const timeElapsed = currentTimeMs - eventTimeMs;
if (timeElapsed <= busyDelayMs) {
// If we haven't yet waited longer than the initial delay, we don't
// have to wait any additional time.
return 0;
}
const msUntilTimeout = busyDelayMs + busyMinDurationMs - timeElapsed;
// This is the value that is passed to `setTimeout`.
return msUntilTimeout;
}
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();
}
}
}
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 & (BlockingMode | ConcurrentMode))) {
return;
}
const tag = fiber.tag;
if (
tag !== IndeterminateComponent &&
tag !== HostRoot &&
tag !== ClassComponent &&
tag !== FunctionComponent &&
tag !== ForwardRef &&
tag !== MemoComponent &&
tag !== SimpleMemoComponent &&
tag !== Block
) {
// 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 = getComponentName(fiber.type) || '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 didWarnStateUpdateForUnmountedComponent: Set<string> | null = null;
function warnAboutUpdateOnUnmountedFiberInDEV(fiber) {
if (__DEV__) {
const tag = fiber.tag;
if (
tag !== HostRoot &&
tag !== ClassComponent &&
tag !== FunctionComponent &&
tag !== ForwardRef &&
tag !== MemoComponent &&
tag !== SimpleMemoComponent &&
tag !== Block
) {
// Only warn for user-defined components, not internal ones like Suspense.
return;
}
// If there are pending passive effects unmounts for this Fiber,
// we can assume that they would have prevented this update.
if ((fiber.effectTag & PassiveUnmountPendingDev) !== NoEffect) {
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 = getComponentName(fiber.type) || 'ReactComponent';
if (didWarnStateUpdateForUnmountedComponent !== null) {
if (didWarnStateUpdateForUnmountedComponent.has(componentName)) {
return;
}
didWarnStateUpdateForUnmountedComponent.add(componentName);
} else {
didWarnStateUpdateForUnmountedComponent = new Set([componentName]);
}
if (isFlushingPassiveEffects) {
// Do not warn if we are currently flushing passive effects!
//
// React can't directly detect a memory leak, but there are some clues that warn about one.
// One of these clues is when an unmounted React component tries to update its state.
// For example, if a component forgets to remove an event listener when unmounting,
// that listener may be called later and try to update state,
// at which point React would warn about the potential leak.
//
// Warning signals are the most useful when they're strong.
// (So we should avoid false positive warnings.)
// Updating state from within an effect cleanup function is sometimes a necessary pattern, e.g.:
// 1. Updating an ancestor that a component had registered itself with on mount.
// 2. Resetting state when a component is hidden after going offscreen.
} else {
const previousFiber = ReactCurrentFiberCurrent;
try {
setCurrentDebugFiberInDEV(fiber);
console.error(
"Can't perform a React state update on an unmounted component. This " +
'is a no-op, but it indicates a memory leak in your application. To ' +
'fix, cancel all subscriptions and asynchronous tasks in %s.',
tag === ClassComponent
? 'the componentWillUnmount method'
: 'a useEffect cleanup function',
);
} 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);
// 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();
// `invokeGuardedCallback` sometimes sets an expando `_suppressLogging`.
// Rethrow this error instead of the original one.
throw replayError;
} else {
// This branch is reachable if the render phase is impure.
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 && getComponentName(workInProgress.type)) ||
'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 =
getComponentName(fiber.type) || '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://fb.me/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;
}
}
}
}
}
// a 'shared' variable that changes when act() opens/closes in tests.
export const IsThisRendererActing = {current: (false: boolean)};
export function warnIfNotScopedWithMatchingAct(fiber: Fiber): void {
if (__DEV__) {
if (
warnsIfNotActing === true &&
IsSomeRendererActing.current === true &&
IsThisRendererActing.current !== true
) {
const previousFiber = ReactCurrentFiberCurrent;
try {
setCurrentDebugFiberInDEV(fiber);
console.error(
"It looks like you're using the wrong act() around your test interactions.\n" +
'Be sure to use the matching version of act() corresponding to your renderer:\n\n' +
'// for react-dom:\n' +
// Break up imports to avoid accidentally parsing them as dependencies.
'import {act} fr' +
"om 'react-dom/test-utils';\n" +
'// ...\n' +
'act(() => ...);\n\n' +
'// for react-test-renderer:\n' +
// Break up imports to avoid accidentally parsing them as dependencies.
'import TestRenderer fr' +
"om react-test-renderer';\n" +
'const {act} = TestRenderer;\n' +
'// ...\n' +
'act(() => ...);',
);
} finally {
if (previousFiber) {
setCurrentDebugFiberInDEV(fiber);
} else {
resetCurrentDebugFiberInDEV();
}
}
}
}
}
export function warnIfNotCurrentlyActingEffectsInDEV(fiber: Fiber): void {
if (__DEV__) {
if (
warnsIfNotActing === true &&
(fiber.mode & StrictMode) !== NoMode &&
IsSomeRendererActing.current === false &&
IsThisRendererActing.current === 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://fb.me/react-wrap-tests-with-act',
getComponentName(fiber.type),
);
}
}
}
function warnIfNotCurrentlyActingUpdatesInDEV(fiber: Fiber): void {
if (__DEV__) {
if (
warnsIfNotActing === true &&
executionContext === NoContext &&
IsSomeRendererActing.current === false &&
IsThisRendererActing.current === false
) {
const previousFiber = ReactCurrentFiberCurrent;
try {
setCurrentDebugFiberInDEV(fiber);
console.error(
'An update to %s inside a test 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://fb.me/react-wrap-tests-with-act',
getComponentName(fiber.type),
);
} finally {
if (previousFiber) {
setCurrentDebugFiberInDEV(fiber);
} else {
resetCurrentDebugFiberInDEV();
}
}
}
}
}
export const warnIfNotCurrentlyActingUpdatesInDev = warnIfNotCurrentlyActingUpdatesInDEV;
// In tests, we want to enforce a mocked scheduler.
let didWarnAboutUnmockedScheduler = false;
// TODO Before we release concurrent mode, revisit this and decide whether a mocked
// scheduler is the actual recommendation. The alternative could be a testing build,
// a new lib, or whatever; we dunno just yet. This message is for early adopters
// to get their tests right.
export function warnIfUnmockedScheduler(fiber: Fiber) {
if (__DEV__) {
if (
didWarnAboutUnmockedScheduler === false &&
Scheduler.unstable_flushAllWithoutAsserting === undefined
) {
if (fiber.mode & BlockingMode || fiber.mode & ConcurrentMode) {
didWarnAboutUnmockedScheduler = true;
console.error(
'In Concurrent or Sync modes, the "scheduler" module needs to be mocked ' +
'to guarantee consistent behaviour across tests and browsers. ' +
'For example, with jest: \n' +
// Break up requires to avoid accidentally parsing them as dependencies.
"jest.mock('scheduler', () => require" +
"('scheduler/unstable_mock'));\n\n" +
'For more info, visit https://fb.me/react-mock-scheduler',
);
} else if (warnAboutUnmockedScheduler === true) {
didWarnAboutUnmockedScheduler = true;
console.error(
'Starting from React v18, the "scheduler" module will need to be mocked ' +
'to guarantee consistent behaviour across tests and browsers. ' +
'For example, with jest: \n' +
// Break up requires to avoid accidentally parsing them as dependencies.
"jest.mock('scheduler', () => require" +
"('scheduler/unstable_mock'));\n\n" +
'For more info, visit https://fb.me/react-mock-scheduler',
);
}
}
}
}
function computeThreadID(root: FiberRoot, lane: Lane | Lanes) {
// Interaction threads are unique per root and expiration time.
// NOTE: Intentionally unsound cast. All that matters is that it's a number
// and it represents a batch of work. Could make a helper function instead,
// but meh this is fine for now.
return (lane: any) * 1000 + root.interactionThreadID;
}
export function markSpawnedWork(lane: Lane | Lanes) {
if (!enableSchedulerTracing) {
return;
}
if (spawnedWorkDuringRender === null) {
spawnedWorkDuringRender = [lane];
} else {
spawnedWorkDuringRender.push(lane);
}
}
function scheduleInteractions(
root: FiberRoot,
lane: Lane | Lanes,
interactions: Set<Interaction>,
) {
if (!enableSchedulerTracing) {
return;
}
if (interactions.size > 0) {
const pendingInteractionMap = root.pendingInteractionMap;
const pendingInteractions = pendingInteractionMap.get(lane);
if (pendingInteractions != null) {
interactions.forEach(interaction => {
if (!pendingInteractions.has(interaction)) {
// Update the pending async work count for previously unscheduled interaction.
interaction.__count++;
}
pendingInteractions.add(interaction);
});
} else {
pendingInteractionMap.set(lane, new Set(interactions));
// Update the pending async work count for the current interactions.
interactions.forEach(interaction => {
interaction.__count++;
});
}
const subscriber = __subscriberRef.current;
if (subscriber !== null) {
const threadID = computeThreadID(root, lane);
subscriber.onWorkScheduled(interactions, threadID);
}
}
}
function schedulePendingInteractions(root: FiberRoot, lane: Lane | Lanes) {
// This is called when work is scheduled on a root.
// It associates the current interactions with the newly-scheduled expiration.
// They will be restored when that expiration is later committed.
if (!enableSchedulerTracing) {
return;
}
scheduleInteractions(root, lane, __interactionsRef.current);
}
function startWorkOnPendingInteractions(root: FiberRoot, lanes: Lanes) {
// This is called when new work is started on a root.
if (!enableSchedulerTracing) {
return;
}
// Determine which interactions this batch of work currently includes, So that
// we can accurately attribute time spent working on it, And so that cascading
// work triggered during the render phase will be associated with it.
const interactions: Set<Interaction> = new Set();
root.pendingInteractionMap.forEach((scheduledInteractions, scheduledLane) => {
if (includesSomeLane(lanes, scheduledLane)) {
scheduledInteractions.forEach(interaction =>
interactions.add(interaction),
);
}
});
// Store the current set of interactions on the FiberRoot for a few reasons:
// We can re-use it in hot functions like performConcurrentWorkOnRoot()
// without having to recalculate it. We will also use it in commitWork() to
// pass to any Profiler onRender() hooks. This also provides DevTools with a
// way to access it when the onCommitRoot() hook is called.
root.memoizedInteractions = interactions;
if (interactions.size > 0) {
const subscriber = __subscriberRef.current;
if (subscriber !== null) {
const threadID = computeThreadID(root, lanes);
try {
subscriber.onWorkStarted(interactions, threadID);
} catch (error) {
// If the subscriber throws, rethrow it in a separate task
scheduleCallback(ImmediateSchedulerPriority, () => {
throw error;
});
}
}
}
}
function finishPendingInteractions(root, committedLanes) {
if (!enableSchedulerTracing) {
return;
}
const remainingLanesAfterCommit = root.pendingLanes;
let subscriber;
try {
subscriber = __subscriberRef.current;
if (subscriber !== null && root.memoizedInteractions.size > 0) {
// FIXME: More than one lane can finish in a single commit.
const threadID = computeThreadID(root, committedLanes);
subscriber.onWorkStopped(root.memoizedInteractions, threadID);
}
} catch (error) {
// If the subscriber throws, rethrow it in a separate task
scheduleCallback(ImmediateSchedulerPriority, () => {
throw error;
});
} finally {
// Clear completed interactions from the pending Map.
// Unless the render was suspended or cascading work was scheduled,
// In which case leave pending interactions until the subsequent render.
const pendingInteractionMap = root.pendingInteractionMap;
pendingInteractionMap.forEach((scheduledInteractions, lane) => {
// Only decrement the pending interaction count if we're done.
// If there's still work at the current priority,
// That indicates that we are waiting for suspense data.
if (!includesSomeLane(remainingLanesAfterCommit, lane)) {
pendingInteractionMap.delete(lane);
scheduledInteractions.forEach(interaction => {
interaction.__count--;
if (subscriber !== null && interaction.__count === 0) {
try {
subscriber.onInteractionScheduledWorkCompleted(interaction);
} catch (error) {
// If the subscriber throws, rethrow it in a separate task
scheduleCallback(ImmediateSchedulerPriority, () => {
throw error;
});
}
}
});
}
});
}
}
// `act` testing API
//
// TODO: This is mostly a copy-paste from the legacy `act`, which does not have
// access to the same internals that we do here. Some trade offs in the
// implementation no longer make sense.
let isFlushingAct = false;
let isInsideThisAct = false;
// TODO: Yes, this is confusing. See above comment. We'll refactor it.
function shouldForceFlushFallbacksInDEV() {
if (!__DEV__) {
// Never force flush in production. This function should get stripped out.
return false;
}
// `IsThisRendererActing.current` is used by ReactTestUtils version of `act`.
if (IsThisRendererActing.current) {
// `isInsideAct` is only used by the reconciler implementation of `act`.
// We don't want to flush suspense fallbacks until the end.
return !isInsideThisAct;
}
// Flush callbacks at the end.
return isFlushingAct;
}
const flushMockScheduler = Scheduler.unstable_flushAllWithoutAsserting;
const isSchedulerMocked = typeof flushMockScheduler === 'function';
// Returns whether additional work was scheduled. Caller should keep flushing
// until there's no work left.
function flushActWork(): boolean {
if (flushMockScheduler !== undefined) {
const prevIsFlushing = isFlushingAct;
isFlushingAct = true;
try {
return flushMockScheduler();
} finally {
isFlushingAct = prevIsFlushing;
}
} else {
// No mock scheduler available. However, the only type of pending work is
// passive effects, which we control. So we can flush that.
const prevIsFlushing = isFlushingAct;
isFlushingAct = true;
try {
let didFlushWork = false;
while (flushPassiveEffects()) {
didFlushWork = true;
}
return didFlushWork;
} finally {
isFlushingAct = prevIsFlushing;
}
}
}
function flushWorkAndMicroTasks(onDone: (err: ?Error) => void) {
try {
flushActWork();
enqueueTask(() => {
if (flushActWork()) {
flushWorkAndMicroTasks(onDone);
} else {
onDone();
}
});
} catch (err) {
onDone(err);
}
}
// we track the 'depth' of the act() calls with this counter,
// so we can tell if any async act() calls try to run in parallel.
let actingUpdatesScopeDepth = 0;
let didWarnAboutUsingActInProd = false;
export function act(callback: () => Thenable<mixed>): Thenable<void> {
if (!__DEV__) {
if (didWarnAboutUsingActInProd === false) {
didWarnAboutUsingActInProd = true;
// eslint-disable-next-line react-internal/no-production-logging
console.error(
'act(...) is not supported in production builds of React, and might not behave as expected.',
);
}
}
const previousActingUpdatesScopeDepth = actingUpdatesScopeDepth;
actingUpdatesScopeDepth++;
const previousIsSomeRendererActing = IsSomeRendererActing.current;
const previousIsThisRendererActing = IsThisRendererActing.current;
const previousIsInsideThisAct = isInsideThisAct;
IsSomeRendererActing.current = true;
IsThisRendererActing.current = true;
isInsideThisAct = true;
function onDone() {
actingUpdatesScopeDepth--;
IsSomeRendererActing.current = previousIsSomeRendererActing;
IsThisRendererActing.current = previousIsThisRendererActing;
isInsideThisAct = previousIsInsideThisAct;
if (__DEV__) {
if (actingUpdatesScopeDepth > previousActingUpdatesScopeDepth) {
// if it's _less than_ previousActingUpdatesScopeDepth, then we can assume the 'other' one has warned
console.error(
'You seem to have overlapping act() calls, this is not supported. ' +
'Be sure to await previous act() calls before making a new one. ',
);
}
}
}
let result;
try {
result = batchedUpdates(callback);
} catch (error) {
// on sync errors, we still want to 'cleanup' and decrement actingUpdatesScopeDepth
onDone();
throw error;
}
if (
result !== null &&
typeof result === 'object' &&
typeof result.then === 'function'
) {
// setup a boolean that gets set to true only
// once this act() call is await-ed
let called = false;
if (__DEV__) {
if (typeof Promise !== 'undefined') {
//eslint-disable-next-line no-undef
Promise.resolve()
.then(() => {})
.then(() => {
if (called === false) {
console.error(
'You called act(async () => ...) without await. ' +
'This could lead to unexpected testing behaviour, interleaving multiple act ' +
'calls and mixing their scopes. You should - await act(async () => ...);',
);
}
});
}
}
// in the async case, the returned thenable runs the callback, flushes
// effects and microtasks in a loop until flushPassiveEffects() === false,
// and cleans up
return {
then(resolve, reject) {
called = true;
result.then(
() => {
if (
actingUpdatesScopeDepth > 1 ||
(isSchedulerMocked === true &&
previousIsSomeRendererActing === true)
) {
onDone();
resolve();
return;
}
// we're about to exit the act() scope,
// now's the time to flush tasks/effects
flushWorkAndMicroTasks((err: ?Error) => {
onDone();
if (err) {
reject(err);
} else {
resolve();
}
});
},
err => {
onDone();
reject(err);
},
);
},
};
} else {
if (__DEV__) {
if (result !== undefined) {
console.error(
'The callback passed to act(...) function ' +
'must return undefined, or a Promise. You returned %s',
result,
);
}
}
// flush effects until none remain, and cleanup
try {
if (
actingUpdatesScopeDepth === 1 &&
(isSchedulerMocked === false || previousIsSomeRendererActing === false)
) {
// we're about to exit the act() scope,
// now's the time to flush effects
flushActWork();
}
onDone();
} catch (err) {
onDone();
throw err;
}
// in the sync case, the returned thenable only warns *if* await-ed
return {
then(resolve) {
if (__DEV__) {
console.error(
'Do not await the result of calling act(...) with sync logic, it is not a Promise.',
);
}
resolve();
},
};
}
}
function detachFiberAfterEffects(fiber: Fiber): void {
fiber.sibling = null;
fiber.stateNode = null;
}