mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
3855 lines
128 KiB
JavaScript
3855 lines
128 KiB
JavaScript
/**
|
||
* 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;
|
||
}
|