Files
react/packages/react-reconciler/src/ReactFiberWorkLoop.js
T
Brian Vaughn 801feed95c Interaction tracing works across hidden and SSR hydration boundaries (#15872)
* Interaction tracing works across hidden and SSR hydration boundaries
2019-06-14 18:08:23 -07:00

2682 lines
92 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/
import type {Fiber} from './ReactFiber';
import type {FiberRoot} from './ReactFiberRoot';
import type {ExpirationTime} from './ReactFiberExpirationTime';
import type {
ReactPriorityLevel,
SchedulerCallback,
} from './SchedulerWithReactIntegration';
import type {Interaction} from 'scheduler/src/Tracing';
import type {SuspenseConfig} from './ReactFiberSuspenseConfig';
import {
warnAboutDeprecatedLifecycles,
enableUserTimingAPI,
enableSuspenseServerRenderer,
replayFailedUnitOfWorkWithInvokeGuardedCallback,
enableProfilerTimer,
enableSchedulerTracing,
revertPassiveEffectsChange,
} from 'shared/ReactFeatureFlags';
import ReactSharedInternals from 'shared/ReactSharedInternals';
import invariant from 'shared/invariant';
import warning from 'shared/warning';
import {
scheduleCallback,
cancelCallback,
getCurrentPriorityLevel,
runWithPriority,
shouldYield,
now,
ImmediatePriority,
UserBlockingPriority,
NormalPriority,
LowPriority,
IdlePriority,
flushSyncCallbackQueue,
scheduleSyncCallback,
} from './SchedulerWithReactIntegration';
import {__interactionsRef, __subscriberRef} from 'scheduler/tracing';
import {
prepareForCommit,
resetAfterCommit,
scheduleTimeout,
cancelTimeout,
noTimeout,
} from './ReactFiberHostConfig';
import {createWorkInProgress, assignFiberPropertiesInDEV} from './ReactFiber';
import {
NoMode,
ProfileMode,
BatchedMode,
ConcurrentMode,
} from './ReactTypeOfMode';
import {
HostRoot,
ClassComponent,
SuspenseComponent,
DehydratedSuspenseComponent,
FunctionComponent,
ForwardRef,
MemoComponent,
SimpleMemoComponent,
} from 'shared/ReactWorkTags';
import {
NoEffect,
PerformedWork,
Placement,
Update,
PlacementAndUpdate,
Deletion,
Ref,
ContentReset,
Snapshot,
Callback,
Passive,
Incomplete,
HostEffectMask,
} from 'shared/ReactSideEffectTags';
import {
NoWork,
Sync,
Never,
msToExpirationTime,
expirationTimeToMs,
computeInteractiveExpiration,
computeAsyncExpiration,
computeSuspenseExpiration,
inferPriorityFromExpirationTime,
LOW_PRIORITY_EXPIRATION,
Batched,
} from './ReactFiberExpirationTime';
import {beginWork as originalBeginWork} from './ReactFiberBeginWork';
import {completeWork} from './ReactFiberCompleteWork';
import {unwindWork, unwindInterruptedWork} from './ReactFiberUnwindWork';
import {
throwException,
createRootErrorUpdate,
createClassErrorUpdate,
} from './ReactFiberThrow';
import {
commitBeforeMutationLifeCycles as commitBeforeMutationEffectOnFiber,
commitLifeCycles as commitLayoutEffectOnFiber,
commitPassiveHookEffects,
commitPlacement,
commitWork,
commitDeletion,
commitDetachRef,
commitAttachRef,
commitResetTextContent,
} from './ReactFiberCommitWork';
import {enqueueUpdate} from './ReactUpdateQueue';
import {resetContextDependencies} from './ReactFiberNewContext';
import {resetHooks, ContextOnlyDispatcher} from './ReactFiberHooks';
import {createCapturedValue} from './ReactCapturedValue';
import {
recordCommitTime,
startProfilerTimer,
stopProfilerTimerIfRunningAndRecordDelta,
} from './ReactProfilerTimer';
// DEV stuff
import warningWithoutStack from 'shared/warningWithoutStack';
import getComponentName from 'shared/getComponentName';
import ReactStrictModeWarnings from './ReactStrictModeWarnings';
import {
phase as ReactCurrentDebugFiberPhaseInDEV,
resetCurrentFiber as resetCurrentDebugFiberInDEV,
setCurrentFiber as setCurrentDebugFiberInDEV,
getStackByFiberInDevAndProd,
} from './ReactCurrentFiber';
import {
recordEffect,
recordScheduleUpdate,
startRequestCallbackTimer,
stopRequestCallbackTimer,
startWorkTimer,
stopWorkTimer,
stopFailedWorkTimer,
startWorkLoopTimer,
stopWorkLoopTimer,
startCommitTimer,
stopCommitTimer,
startCommitSnapshotEffectsTimer,
stopCommitSnapshotEffectsTimer,
startCommitHostEffectsTimer,
stopCommitHostEffectsTimer,
startCommitLifeCyclesTimer,
stopCommitLifeCyclesTimer,
} from './ReactDebugFiberPerf';
import {
invokeGuardedCallback,
hasCaughtError,
clearCaughtError,
} from 'shared/ReactErrorUtils';
import {onCommitRoot} from './ReactFiberDevToolsHook';
const ceil = Math.ceil;
const {
ReactCurrentDispatcher,
ReactCurrentOwner,
ReactCurrentActingRendererSigil,
} = ReactSharedInternals;
type ExecutionContext = number;
const NoContext = /* */ 0b000000;
const BatchedContext = /* */ 0b000001;
const EventContext = /* */ 0b000010;
const DiscreteEventContext = /* */ 0b000100;
const LegacyUnbatchedContext = /* */ 0b001000;
const RenderContext = /* */ 0b010000;
const CommitContext = /* */ 0b100000;
type RootExitStatus = 0 | 1 | 2 | 3 | 4;
const RootIncomplete = 0;
const RootErrored = 1;
const RootSuspended = 2;
const RootSuspendedWithDelay = 3;
const RootCompleted = 4;
export type Thenable = {
then(resolve: () => mixed, reject?: () => mixed): Thenable | void,
};
// 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 expiration time we're rendering
let renderExpirationTime: ExpirationTime = NoWork;
// Whether to root completed, errored, suspended, etc.
let workInProgressRootExitStatus: RootExitStatus = RootIncomplete;
// Most recent event time among processed updates during this render.
// This is conceptually a time stamp but expressed in terms of an ExpirationTime
// because we deal mostly with expiration times in the hot path, so this avoids
// the conversion happening in the hot path.
let workInProgressRootLatestProcessedExpirationTime: ExpirationTime = Sync;
let workInProgressRootLatestSuspenseTimeout: ExpirationTime = Sync;
let workInProgressRootCanSuspendUsingConfig: null | SuspenseConfig = null;
// If we're pinged while rendering we don't always restart immediately.
// This flag determines if it might be worthwhile to restart if an opportunity
// happens latere.
let workInProgressRootHasPendingPing: boolean = false;
// 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;
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 pendingPassiveEffectsExpirationTime: ExpirationTime = NoWork;
let rootsWithPendingDiscreteUpdates: Map<
FiberRoot,
ExpirationTime,
> | 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;
let interruptedBy: Fiber | null = null;
// Marks the need to reschedule pending interactions at Never priority during the commit phase.
// This enables them to be traced accross hidden boundaries or suspended SSR hydration.
let didDeprioritizeIdleSubtree: boolean = false;
// Expiration times are computed by adding to the current time (the start
// time). However, if two updates are scheduled within the same event, we
// should treat their start times as simultaneous, even if the actual clock
// time has advanced between the first and second call.
// In other words, because expiration times determine how updates are batched,
// we want all updates of like priority that occur within the same event to
// receive the same expiration time. Otherwise we get tearing.
let currentEventTime: ExpirationTime = NoWork;
export function requestCurrentTime() {
if ((executionContext & (RenderContext | CommitContext)) !== NoContext) {
// We're inside React, so it's fine to read the actual time.
return msToExpirationTime(now());
}
// We're not inside React, so we may be in the middle of a browser event.
if (currentEventTime !== NoWork) {
// 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 = msToExpirationTime(now());
return currentEventTime;
}
export function computeExpirationForFiber(
currentTime: ExpirationTime,
fiber: Fiber,
suspenseConfig: null | SuspenseConfig,
): ExpirationTime {
const mode = fiber.mode;
if ((mode & BatchedMode) === NoMode) {
return Sync;
}
const priorityLevel = getCurrentPriorityLevel();
if ((mode & ConcurrentMode) === NoMode) {
return priorityLevel === ImmediatePriority ? Sync : Batched;
}
if ((executionContext & RenderContext) !== NoContext) {
// Use whatever time we're already rendering
return renderExpirationTime;
}
let expirationTime;
if (suspenseConfig !== null) {
// Compute an expiration time based on the Suspense timeout.
expirationTime = computeSuspenseExpiration(
currentTime,
suspenseConfig.timeoutMs | 0 || LOW_PRIORITY_EXPIRATION,
);
} else {
// Compute an expiration time based on the Scheduler priority.
switch (priorityLevel) {
case ImmediatePriority:
expirationTime = Sync;
break;
case UserBlockingPriority:
// TODO: Rename this to computeUserBlockingExpiration
expirationTime = computeInteractiveExpiration(currentTime);
break;
case NormalPriority:
case LowPriority: // TODO: Handle LowPriority
// TODO: Rename this to... something better.
expirationTime = computeAsyncExpiration(currentTime);
break;
case IdlePriority:
expirationTime = Never;
break;
default:
invariant(false, 'Expected a valid priority level');
}
}
// If we're in the middle of rendering a tree, do not update at the same
// expiration time that is already rendering.
// TODO: We shouldn't have to do this if the update is on a different root.
// Refactor computeExpirationForFiber + scheduleUpdate so we have access to
// the root when we check for this condition.
if (workInProgressRoot !== null && expirationTime === renderExpirationTime) {
// This is a trick to move this update into a separate batch
expirationTime -= 1;
}
return expirationTime;
}
let lastUniqueAsyncExpiration = NoWork;
export function computeUniqueAsyncExpiration(): ExpirationTime {
const currentTime = requestCurrentTime();
let result = computeAsyncExpiration(currentTime);
if (result <= lastUniqueAsyncExpiration) {
// Since we assume the current time monotonically increases, we only hit
// this branch when computeUniqueAsyncExpiration is fired multiple times
// within a 200ms window (or whatever the async bucket size is).
result -= 1;
}
lastUniqueAsyncExpiration = result;
return result;
}
export function scheduleUpdateOnFiber(
fiber: Fiber,
expirationTime: ExpirationTime,
) {
checkForNestedUpdates();
warnAboutInvalidUpdatesOnClassComponentsInDEV(fiber);
const root = markUpdateTimeFromFiberToRoot(fiber, expirationTime);
if (root === null) {
warnAboutUpdateOnUnmountedFiberInDEV(fiber);
return;
}
root.pingTime = NoWork;
checkForInterruption(fiber, expirationTime);
recordScheduleUpdate();
// TODO: computeExpirationForFiber also reads the priority. Pass the
// priority as an argument to that function and this one.
const priorityLevel = getCurrentPriorityLevel();
if (expirationTime === Sync) {
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, expirationTime);
// 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.
let callback = renderRoot(root, Sync, true);
while (callback !== null) {
callback = callback(true);
}
} else {
scheduleCallbackForRoot(root, ImmediatePriority, Sync);
if (executionContext === NoContext) {
// Flush the synchronous work now, wnless 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-initated
// updates, to preserve historical behavior of sync mode.
flushSyncCallbackQueue();
}
}
} else {
scheduleCallbackForRoot(root, priorityLevel, expirationTime);
}
if (
(executionContext & DiscreteEventContext) !== NoContext &&
// Only updates at user-blocking priority or greater are considered
// discrete, even inside a discrete event.
(priorityLevel === UserBlockingPriority ||
priorityLevel === ImmediatePriority)
) {
// 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 Map([[root, expirationTime]]);
} else {
const lastDiscreteTime = rootsWithPendingDiscreteUpdates.get(root);
if (lastDiscreteTime === undefined || lastDiscreteTime > expirationTime) {
rootsWithPendingDiscreteUpdates.set(root, expirationTime);
}
}
}
}
export const scheduleWork = scheduleUpdateOnFiber;
// 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 markUpdateTimeFromFiberToRoot(fiber, expirationTime) {
// Update the source fiber's expiration time
if (fiber.expirationTime < expirationTime) {
fiber.expirationTime = expirationTime;
}
let alternate = fiber.alternate;
if (alternate !== null && alternate.expirationTime < expirationTime) {
alternate.expirationTime = expirationTime;
}
// Walk the parent path to the root and update the child expiration time.
let node = fiber.return;
let root = null;
if (node === null && fiber.tag === HostRoot) {
root = fiber.stateNode;
} else {
while (node !== null) {
alternate = node.alternate;
if (node.childExpirationTime < expirationTime) {
node.childExpirationTime = expirationTime;
if (
alternate !== null &&
alternate.childExpirationTime < expirationTime
) {
alternate.childExpirationTime = expirationTime;
}
} else if (
alternate !== null &&
alternate.childExpirationTime < expirationTime
) {
alternate.childExpirationTime = expirationTime;
}
if (node.return === null && node.tag === HostRoot) {
root = node.stateNode;
break;
}
node = node.return;
}
}
if (root !== null) {
// Update the first and last pending expiration times in this root
const firstPendingTime = root.firstPendingTime;
if (expirationTime > firstPendingTime) {
root.firstPendingTime = expirationTime;
}
const lastPendingTime = root.lastPendingTime;
if (lastPendingTime === NoWork || expirationTime < lastPendingTime) {
root.lastPendingTime = expirationTime;
}
}
return root;
}
// Use this function, along with runRootCallback, to ensure that only a single
// callback per root is scheduled. It's still possible to call renderRoot
// directly, but scheduling via this function helps avoid excessive callbacks.
// It works by storing the callback node and expiration time on the root. When a
// new callback comes in, it compares the expiration time to determine if it
// should cancel the previous one. It also relies on commitRoot scheduling a
// callback to render the next level, because that means we don't need a
// separate callback per expiration time.
function scheduleCallbackForRoot(
root: FiberRoot,
priorityLevel: ReactPriorityLevel,
expirationTime: ExpirationTime,
) {
const existingCallbackExpirationTime = root.callbackExpirationTime;
if (existingCallbackExpirationTime < expirationTime) {
// New callback has higher priority than the existing one.
const existingCallbackNode = root.callbackNode;
if (existingCallbackNode !== null) {
cancelCallback(existingCallbackNode);
}
root.callbackExpirationTime = expirationTime;
if (expirationTime === Sync) {
// Sync React callbacks are scheduled on a special internal queue
root.callbackNode = scheduleSyncCallback(
runRootCallback.bind(
null,
root,
renderRoot.bind(null, root, expirationTime),
),
);
} else {
let options = null;
if (expirationTime !== Never) {
let timeout = expirationTimeToMs(expirationTime) - now();
options = {timeout};
}
root.callbackNode = scheduleCallback(
priorityLevel,
runRootCallback.bind(
null,
root,
renderRoot.bind(null, root, expirationTime),
),
options,
);
if (
enableUserTimingAPI &&
expirationTime !== Sync &&
(executionContext & (RenderContext | CommitContext)) === NoContext
) {
// Scheduled an async callback, and we're not already working. Add an
// entry to the flamegraph that shows we're waiting for a callback
// to fire.
startRequestCallbackTimer();
}
}
}
// Associate the current interactions with this new root+priority.
schedulePendingInteractions(root, expirationTime);
}
function runRootCallback(root, callback, isSync) {
const prevCallbackNode = root.callbackNode;
let continuation = null;
try {
continuation = callback(isSync);
if (continuation !== null) {
return runRootCallback.bind(null, root, continuation);
} else {
return null;
}
} finally {
// If the callback exits without returning a continuation, remove the
// corresponding callback node from the root. Unless the callback node
// has changed, which implies that it was already cancelled by a high
// priority update.
if (continuation === null && prevCallbackNode === root.callbackNode) {
root.callbackNode = null;
root.callbackExpirationTime = NoWork;
}
}
}
export function flushRoot(root: FiberRoot, expirationTime: ExpirationTime) {
if ((executionContext & (RenderContext | CommitContext)) !== NoContext) {
invariant(
false,
'work.commit(): Cannot commit while already rendering. This likely ' +
'means you attempted to commit from inside a lifecycle method.',
);
}
scheduleSyncCallback(renderRoot.bind(null, root, expirationTime));
flushSyncCallbackQueue();
}
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__ && (executionContext & RenderContext) !== NoContext) {
warning(
false,
'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 (!revertPassiveEffectsChange) {
// If the discrete updates scheduled passive effects, flush them now so that
// they fire before the next serial event.
flushPassiveEffects();
}
}
function resolveLocksOnRoot(root: FiberRoot, expirationTime: ExpirationTime) {
const firstBatch = root.firstBatch;
if (
firstBatch !== null &&
firstBatch._defer &&
firstBatch._expirationTime >= expirationTime
) {
scheduleCallback(NormalPriority, () => {
firstBatch._onComplete();
return null;
});
return true;
} else {
return false;
}
}
export function deferredUpdates<A>(fn: () => A): A {
// TODO: Remove in favor of Scheduler.next
return runWithPriority(NormalPriority, fn);
}
export function syncUpdates<A, B, C, R>(
fn: (A, B, C) => R,
a: A,
b: B,
c: C,
): R {
return runWithPriority(ImmediatePriority, fn.bind(null, a, b, c));
}
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((expirationTime, root) => {
scheduleSyncCallback(renderRoot.bind(null, root, expirationTime));
});
// 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, R>(
fn: (A, B, C) => R,
a: A,
b: B,
c: C,
): R {
const prevExecutionContext = executionContext;
executionContext |= DiscreteEventContext;
try {
// Should this
return runWithPriority(UserBlockingPriority, fn.bind(null, a, b, c));
} 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 {
if ((executionContext & (RenderContext | CommitContext)) !== NoContext) {
invariant(
false,
'flushSync was called from inside a lifecycle method. It cannot be ' +
'called when React is already rendering.',
);
}
const prevExecutionContext = executionContext;
executionContext |= BatchedContext;
try {
return runWithPriority(ImmediatePriority, fn.bind(null, a));
} 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;
try {
runWithPriority(ImmediatePriority, fn);
} finally {
executionContext = prevExecutionContext;
if (executionContext === NoContext) {
// Flush the immediate callbacks that were scheduled during this batch
flushSyncCallbackQueue();
}
}
}
function prepareFreshStack(root, expirationTime) {
root.finishedWork = null;
root.finishedExpirationTime = NoWork;
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, expirationTime);
renderExpirationTime = expirationTime;
workInProgressRootExitStatus = RootIncomplete;
workInProgressRootLatestProcessedExpirationTime = Sync;
workInProgressRootLatestSuspenseTimeout = Sync;
workInProgressRootCanSuspendUsingConfig = null;
workInProgressRootHasPendingPing = false;
if (enableSchedulerTracing) {
didDeprioritizeIdleSubtree = false;
}
if (__DEV__) {
ReactStrictModeWarnings.discardPendingWarnings();
componentsWithSuspendedDiscreteUpdates = null;
}
}
function renderRoot(
root: FiberRoot,
expirationTime: ExpirationTime,
isSync: boolean,
): SchedulerCallback | null {
invariant(
(executionContext & (RenderContext | CommitContext)) === NoContext,
'Should not already be working.',
);
if (enableUserTimingAPI && expirationTime !== Sync) {
const didExpire = isSync;
stopRequestCallbackTimer(didExpire);
}
if (root.firstPendingTime < expirationTime) {
// If there's no work left at this expiration time, exit immediately. This
// happens when multiple callbacks are scheduled for a single root, but an
// earlier callback flushes the work of a later one.
return null;
}
if (isSync && root.finishedExpirationTime === expirationTime) {
// There's already a pending commit at this expiration time.
// TODO: This is poorly factored. This case only exists for the
// batch.commit() API.
return commitRoot.bind(null, root);
}
flushPassiveEffects();
// If the root or expiration time have changed, throw out the existing stack
// and prepare a fresh one. Otherwise we'll continue where we left off.
if (root !== workInProgressRoot || expirationTime !== renderExpirationTime) {
prepareFreshStack(root, expirationTime);
startWorkOnPendingInteractions(root, expirationTime);
} else if (workInProgressRootExitStatus === RootSuspendedWithDelay) {
// We could've received an update at a lower priority while we yielded.
// We're suspended in a delayed state. Once we complete this render we're
// just going to try to recover at the last pending time anyway so we might
// as well start doing that eagerly.
// Ideally we should be able to do this even for retries but we don't yet
// know if we're going to process an update which wants to commit earlier,
// and this path happens very early so it would happen too often. Instead,
// for that case, we'll wait until we complete.
if (workInProgressRootHasPendingPing) {
// We have a ping at this expiration. Let's restart to see if we get unblocked.
prepareFreshStack(root, expirationTime);
} else {
const lastPendingTime = root.lastPendingTime;
if (lastPendingTime < expirationTime) {
// There's lower priority work. It might be unsuspended. Try rendering
// at that level immediately, while preserving the position in the queue.
return renderRoot.bind(null, root, lastPendingTime);
}
}
}
// If we have a work-in-progress fiber, it means there's still work to do
// in this root.
if (workInProgress !== null) {
const prevExecutionContext = executionContext;
executionContext |= RenderContext;
let prevDispatcher = ReactCurrentDispatcher.current;
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.
prevDispatcher = ContextOnlyDispatcher;
}
ReactCurrentDispatcher.current = ContextOnlyDispatcher;
let prevInteractions: Set<Interaction> | null = null;
if (enableSchedulerTracing) {
prevInteractions = __interactionsRef.current;
__interactionsRef.current = root.memoizedInteractions;
}
startWorkLoopTimer(workInProgress);
// TODO: Fork renderRoot into renderRootSync and renderRootAsync
if (isSync) {
if (expirationTime !== Sync) {
// An async update expired. There may be other expired updates on
// this root. We should render all the expired work in a
// single batch.
const currentTime = requestCurrentTime();
if (currentTime < expirationTime) {
// Restart at the current time.
executionContext = prevExecutionContext;
resetContextDependencies();
ReactCurrentDispatcher.current = prevDispatcher;
if (enableSchedulerTracing) {
__interactionsRef.current = ((prevInteractions: any): Set<
Interaction,
>);
}
return renderRoot.bind(null, root, currentTime);
}
}
} else {
// 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 = NoWork;
}
do {
try {
if (isSync) {
workLoopSync();
} else {
workLoop();
}
break;
} catch (thrownValue) {
// Reset module-level state that was set during the render phase.
resetContextDependencies();
resetHooks();
const sourceFiber = workInProgress;
if (sourceFiber === null || sourceFiber.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.
prepareFreshStack(root, expirationTime);
executionContext = prevExecutionContext;
throw thrownValue;
}
if (enableProfilerTimer && sourceFiber.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(sourceFiber, true);
}
const returnFiber = sourceFiber.return;
throwException(
root,
returnFiber,
sourceFiber,
thrownValue,
renderExpirationTime,
);
workInProgress = completeUnitOfWork(sourceFiber);
}
} while (true);
executionContext = prevExecutionContext;
resetContextDependencies();
ReactCurrentDispatcher.current = prevDispatcher;
if (enableSchedulerTracing) {
__interactionsRef.current = ((prevInteractions: any): Set<Interaction>);
}
if (workInProgress !== null) {
// There's still work left over. Return a continuation.
stopInterruptedWorkLoopTimer();
if (expirationTime !== Sync) {
startRequestCallbackTimer();
}
return renderRoot.bind(null, root, expirationTime);
}
}
// 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.
stopFinishedWorkLoopTimer();
root.finishedWork = root.current.alternate;
root.finishedExpirationTime = expirationTime;
const isLocked = resolveLocksOnRoot(root, expirationTime);
if (isLocked) {
// This root has a lock that prevents it from committing. Exit. If we begin
// work on the root again, without any intervening updates, it will finish
// without doing additional work.
return null;
}
// Set this to null to indicate there's no in-progress render.
workInProgressRoot = null;
switch (workInProgressRootExitStatus) {
case RootIncomplete: {
invariant(false, 'Should have a work-in-progress.');
}
// Flow knows about invariant, so it compains 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: {
// An error was thrown. First check if there is lower priority work
// scheduled on this root.
const lastPendingTime = root.lastPendingTime;
if (lastPendingTime < expirationTime) {
// There's lower priority work. Before raising the error, try rendering
// at the lower priority to see if it fixes it. Use a continuation to
// maintain the existing priority and position in the queue.
return renderRoot.bind(null, root, lastPendingTime);
}
if (!isSync) {
// If we're rendering asynchronously, it's possible the error was
// caused by tearing due to a mutation during an event. Try rendering
// one more time without yiedling to events.
prepareFreshStack(root, expirationTime);
scheduleSyncCallback(renderRoot.bind(null, root, expirationTime));
return null;
}
// If we're already rendering synchronously, commit the root in its
// errored state.
return commitRoot.bind(null, root);
}
case RootSuspended: {
// We have an acceptable loading state. We need to figure out if we should
// immediately commit it or wait a bit.
// If we have processed new updates during this render, we may now have a
// new loading state ready. We want to ensure that we commit that as soon as
// possible.
const hasNotProcessedNewUpdates =
workInProgressRootLatestProcessedExpirationTime === Sync;
if (hasNotProcessedNewUpdates && !isSync) {
// If we have not processed any new updates during this pass, then this is
// either a retry of an existing fallback state or a hidden tree.
// Hidden trees shouldn't be batched with other work and after that's
// fixed it can only be a retry.
// We're going to throttle committing retries so that we don't show too
// many loading states too quickly.
let msUntilTimeout =
globalMostRecentFallbackTime + FALLBACK_THROTTLE_MS - now();
// Don't bother with a very short suspense time.
if (msUntilTimeout > 10) {
if (workInProgressRootHasPendingPing) {
// This render was pinged but we didn't get to restart earlier so try
// restarting now instead.
prepareFreshStack(root, expirationTime);
return renderRoot.bind(null, root, expirationTime);
}
const lastPendingTime = root.lastPendingTime;
if (lastPendingTime < expirationTime) {
// There's lower priority work. It might be unsuspended. Try rendering
// at that level.
return renderRoot.bind(null, root, lastPendingTime);
}
// 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,
);
return null;
}
}
// The work expired. Commit immediately.
return commitRoot.bind(null, root);
}
case RootSuspendedWithDelay: {
if (!isSync) {
// 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.
if (workInProgressRootHasPendingPing) {
// This render was pinged but we didn't get to restart earlier so try
// restarting now instead.
prepareFreshStack(root, expirationTime);
return renderRoot.bind(null, root, expirationTime);
}
const lastPendingTime = root.lastPendingTime;
if (lastPendingTime < expirationTime) {
// There's lower priority work. It might be unsuspended. Try rendering
// at that level immediately.
return renderRoot.bind(null, root, lastPendingTime);
}
let msUntilTimeout;
if (workInProgressRootLatestSuspenseTimeout !== Sync) {
// We have processed a suspense config whose expiration time we can use as
// the timeout.
msUntilTimeout =
expirationTimeToMs(workInProgressRootLatestSuspenseTimeout) - now();
} else if (workInProgressRootLatestProcessedExpirationTime === Sync) {
// 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 don't have a suspense config, we're going to use a heuristic to
// determine how long we can suspend.
const eventTimeMs: number = inferTimeFromExpirationTime(
workInProgressRootLatestProcessedExpirationTime,
);
const currentTimeMs = now();
const timeUntilExpirationMs =
expirationTimeToMs(expirationTime) - currentTimeMs;
let timeElapsed = currentTimeMs - eventTimeMs;
if (timeElapsed < 0) {
// We get this wrong some time since we estimate the time.
timeElapsed = 0;
}
msUntilTimeout = jnd(timeElapsed) - timeElapsed;
// Clamp the timeout to the expiration time.
// TODO: Once the event time is exact instead of inferred from expiration time
// we don't need this.
if (timeUntilExpirationMs < msUntilTimeout) {
msUntilTimeout = timeUntilExpirationMs;
}
}
// 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,
);
return null;
}
}
// The work expired. Commit immediately.
return commitRoot.bind(null, root);
}
case RootCompleted: {
// The work completed. Ready to commit.
if (
!isSync &&
workInProgressRootLatestProcessedExpirationTime !== Sync &&
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(
workInProgressRootLatestProcessedExpirationTime,
expirationTime,
workInProgressRootCanSuspendUsingConfig,
);
if (msUntilTimeout > 10) {
root.timeoutHandle = scheduleTimeout(
commitRoot.bind(null, root),
msUntilTimeout,
);
return null;
}
}
return commitRoot.bind(null, root);
}
default: {
invariant(false, 'Unknown root exit status.');
}
}
}
export function markCommitTimeOfFallback() {
globalMostRecentFallbackTime = now();
}
export function markRenderEventTimeAndConfig(
expirationTime: ExpirationTime,
suspenseConfig: null | SuspenseConfig,
): void {
if (
expirationTime < workInProgressRootLatestProcessedExpirationTime &&
expirationTime > Never
) {
workInProgressRootLatestProcessedExpirationTime = expirationTime;
}
if (suspenseConfig !== null) {
if (
expirationTime < workInProgressRootLatestSuspenseTimeout &&
expirationTime > Never
) {
workInProgressRootLatestSuspenseTimeout = expirationTime;
// Most of the time we only have one config and getting wrong is not bad.
workInProgressRootCanSuspendUsingConfig = suspenseConfig;
}
}
}
export function renderDidSuspend(): void {
if (workInProgressRootExitStatus === RootIncomplete) {
workInProgressRootExitStatus = RootSuspended;
}
}
export function renderDidSuspendDelayIfPossible(): void {
if (
workInProgressRootExitStatus === RootIncomplete ||
workInProgressRootExitStatus === RootSuspended
) {
workInProgressRootExitStatus = RootSuspendedWithDelay;
}
}
export function renderDidError() {
if (workInProgressRootExitStatus !== RootCompleted) {
workInProgressRootExitStatus = RootErrored;
}
}
function inferTimeFromExpirationTime(expirationTime: ExpirationTime): number {
// We don't know exactly when the update was scheduled, but we can infer an
// approximate start time from the expiration time.
const earliestExpirationTimeMs = expirationTimeToMs(expirationTime);
return earliestExpirationTimeMs - LOW_PRIORITY_EXPIRATION;
}
function inferTimeFromExpirationTimeWithSuspenseConfig(
expirationTime: ExpirationTime,
suspenseConfig: SuspenseConfig,
): number {
// We don't know exactly when the update was scheduled, but we can infer an
// approximate start time from the expiration time by subtracting the timeout
// that was added to the event time.
const earliestExpirationTimeMs = expirationTimeToMs(expirationTime);
return (
earliestExpirationTimeMs -
(suspenseConfig.timeoutMs | 0 || LOW_PRIORITY_EXPIRATION)
);
}
function workLoopSync() {
// Already timed out, so perform work without checking if we need to yield.
while (workInProgress !== null) {
workInProgress = performUnitOfWork(workInProgress);
}
}
function workLoop() {
// Perform work until Scheduler asks us to yield
while (workInProgress !== null && !shouldYield()) {
workInProgress = performUnitOfWork(workInProgress);
}
}
function performUnitOfWork(unitOfWork: Fiber): Fiber | null {
// 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;
startWorkTimer(unitOfWork);
setCurrentDebugFiberInDEV(unitOfWork);
let next;
if (enableProfilerTimer && (unitOfWork.mode & ProfileMode) !== NoMode) {
startProfilerTimer(unitOfWork);
next = beginWork(current, unitOfWork, renderExpirationTime);
stopProfilerTimerIfRunningAndRecordDelta(unitOfWork, true);
} else {
next = beginWork(current, unitOfWork, renderExpirationTime);
}
resetCurrentDebugFiberInDEV();
unitOfWork.memoizedProps = unitOfWork.pendingProps;
if (next === null) {
// If this doesn't spawn new work, complete the current work.
next = completeUnitOfWork(unitOfWork);
}
ReactCurrentOwner.current = null;
return next;
}
function completeUnitOfWork(unitOfWork: Fiber): Fiber | null {
// 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.
workInProgress = 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 = workInProgress.alternate;
const returnFiber = workInProgress.return;
// Check if the work completed or if something threw.
if ((workInProgress.effectTag & Incomplete) === NoEffect) {
setCurrentDebugFiberInDEV(workInProgress);
let next;
if (
!enableProfilerTimer ||
(workInProgress.mode & ProfileMode) === NoMode
) {
next = completeWork(current, workInProgress, renderExpirationTime);
} else {
startProfilerTimer(workInProgress);
next = completeWork(current, workInProgress, renderExpirationTime);
// Update render duration assuming we didn't error.
stopProfilerTimerIfRunningAndRecordDelta(workInProgress, false);
}
stopWorkTimer(workInProgress);
resetCurrentDebugFiberInDEV();
resetChildExpirationTime(workInProgress);
if (next !== null) {
// Completing this fiber spawned new work. Work on that next.
return next;
}
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 = workInProgress.firstEffect;
}
if (workInProgress.lastEffect !== null) {
if (returnFiber.lastEffect !== null) {
returnFiber.lastEffect.nextEffect = workInProgress.firstEffect;
}
returnFiber.lastEffect = workInProgress.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 = workInProgress.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 = workInProgress;
} else {
returnFiber.firstEffect = workInProgress;
}
returnFiber.lastEffect = workInProgress;
}
}
} 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(workInProgress, renderExpirationTime);
// Because this fiber did not complete, don't reset its expiration time.
if (
enableProfilerTimer &&
(workInProgress.mode & ProfileMode) !== NoMode
) {
// Record the render duration for the fiber that errored.
stopProfilerTimerIfRunningAndRecordDelta(workInProgress, false);
// Include the time spent working on failed children before continuing.
let actualDuration = workInProgress.actualDuration;
let child = workInProgress.child;
while (child !== null) {
actualDuration += child.actualDuration;
child = child.sibling;
}
workInProgress.actualDuration = actualDuration;
}
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.
// TODO: The name stopFailedWorkTimer is misleading because Suspense
// also captures and restarts.
stopFailedWorkTimer(workInProgress);
next.effectTag &= HostEffectMask;
return next;
}
stopWorkTimer(workInProgress);
if (returnFiber !== null) {
// Mark the parent fiber as incomplete and clear its effect list.
returnFiber.firstEffect = returnFiber.lastEffect = null;
returnFiber.effectTag |= Incomplete;
}
}
const siblingFiber = workInProgress.sibling;
if (siblingFiber !== null) {
// If there is more work to do in this returnFiber, do that next.
return siblingFiber;
}
// Otherwise, return to the parent
workInProgress = returnFiber;
} while (workInProgress !== null);
// We've reached the root.
if (workInProgressRootExitStatus === RootIncomplete) {
workInProgressRootExitStatus = RootCompleted;
}
return null;
}
function resetChildExpirationTime(completedWork: Fiber) {
if (
renderExpirationTime !== Never &&
completedWork.childExpirationTime === Never
) {
// The children of this component are hidden. Don't bubble their
// expiration times.
return;
}
let newChildExpirationTime = NoWork;
// 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;
// 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) {
const childUpdateExpirationTime = child.expirationTime;
const childChildExpirationTime = child.childExpirationTime;
if (childUpdateExpirationTime > newChildExpirationTime) {
newChildExpirationTime = childUpdateExpirationTime;
}
if (childChildExpirationTime > newChildExpirationTime) {
newChildExpirationTime = childChildExpirationTime;
}
if (shouldBubbleActualDurations) {
actualDuration += child.actualDuration;
}
treeBaseDuration += child.treeBaseDuration;
child = child.sibling;
}
completedWork.actualDuration = actualDuration;
completedWork.treeBaseDuration = treeBaseDuration;
} else {
let child = completedWork.child;
while (child !== null) {
const childUpdateExpirationTime = child.expirationTime;
const childChildExpirationTime = child.childExpirationTime;
if (childUpdateExpirationTime > newChildExpirationTime) {
newChildExpirationTime = childUpdateExpirationTime;
}
if (childChildExpirationTime > newChildExpirationTime) {
newChildExpirationTime = childChildExpirationTime;
}
child = child.sibling;
}
}
completedWork.childExpirationTime = newChildExpirationTime;
}
function commitRoot(root) {
runWithPriority(ImmediatePriority, commitRootImpl.bind(null, root));
// If there are passive effects, schedule a callback to flush them. This goes
// outside commitRootImpl so that it inherits the priority of the render.
if (rootWithPendingPassiveEffects !== null) {
const priorityLevel = getCurrentPriorityLevel();
scheduleCallback(priorityLevel, () => {
flushPassiveEffects();
return null;
});
}
return null;
}
function commitRootImpl(root) {
flushPassiveEffects();
flushRenderPhaseStrictModeWarningsInDEV();
flushSuspensePriorityWarningInDEV();
invariant(
(executionContext & (RenderContext | CommitContext)) === NoContext,
'Should not already be working.',
);
const finishedWork = root.finishedWork;
const expirationTime = root.finishedExpirationTime;
if (finishedWork === null) {
return null;
}
root.finishedWork = null;
root.finishedExpirationTime = NoWork;
invariant(
finishedWork !== root.current,
'Cannot commit the same tree as before. This error is likely caused by ' +
'a bug in React. Please file an issue.',
);
// commitRoot never returns a continuation; it always finishes synchronously.
// So we can clear these now to allow a new callback to be scheduled.
root.callbackNode = null;
root.callbackExpirationTime = NoWork;
startCommitTimer();
// Update the first and last pending times on this root. The new first
// pending time is whatever is left on the root fiber.
const updateExpirationTimeBeforeCommit = finishedWork.expirationTime;
const childExpirationTimeBeforeCommit = finishedWork.childExpirationTime;
const firstPendingTimeBeforeCommit =
childExpirationTimeBeforeCommit > updateExpirationTimeBeforeCommit
? childExpirationTimeBeforeCommit
: updateExpirationTimeBeforeCommit;
root.firstPendingTime = firstPendingTimeBeforeCommit;
if (firstPendingTimeBeforeCommit < root.lastPendingTime) {
// This usually means we've finished all the work, but it can also happen
// when something gets downprioritized during render, like a hidden tree.
root.lastPendingTime = firstPendingTimeBeforeCommit;
}
if (root === workInProgressRoot) {
// We can reset these now that they are finished.
workInProgressRoot = null;
workInProgress = null;
renderExpirationTime = NoWork;
} 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) {
const prevExecutionContext = executionContext;
executionContext |= CommitContext;
let prevInteractions: Set<Interaction> | null = null;
if (enableSchedulerTracing) {
prevInteractions = __interactionsRef.current;
__interactionsRef.current = root.memoizedInteractions;
}
// 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.
startCommitSnapshotEffectsTimer();
prepareForCommit(root.containerInfo);
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);
stopCommitSnapshotEffectsTimer();
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.
startCommitHostEffectsTimer();
nextEffect = firstEffect;
do {
if (__DEV__) {
invokeGuardedCallback(null, commitMutationEffects, null);
if (hasCaughtError()) {
invariant(nextEffect !== null, 'Should be working on an effect.');
const error = clearCaughtError();
captureCommitPhaseError(nextEffect, error);
nextEffect = nextEffect.nextEffect;
}
} else {
try {
commitMutationEffects();
} catch (error) {
invariant(nextEffect !== null, 'Should be working on an effect.');
captureCommitPhaseError(nextEffect, error);
nextEffect = nextEffect.nextEffect;
}
}
} while (nextEffect !== null);
stopCommitHostEffectsTimer();
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.
startCommitLifeCyclesTimer();
nextEffect = firstEffect;
do {
if (__DEV__) {
invokeGuardedCallback(
null,
commitLayoutEffects,
null,
root,
expirationTime,
);
if (hasCaughtError()) {
invariant(nextEffect !== null, 'Should be working on an effect.');
const error = clearCaughtError();
captureCommitPhaseError(nextEffect, error);
nextEffect = nextEffect.nextEffect;
}
} else {
try {
commitLayoutEffects(root, expirationTime);
} catch (error) {
invariant(nextEffect !== null, 'Should be working on an effect.');
captureCommitPhaseError(nextEffect, error);
nextEffect = nextEffect.nextEffect;
}
}
} while (nextEffect !== null);
stopCommitLifeCyclesTimer();
nextEffect = null;
if (enableSchedulerTracing) {
__interactionsRef.current = ((prevInteractions: any): Set<Interaction>);
}
executionContext = prevExecutionContext;
} 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.
startCommitSnapshotEffectsTimer();
stopCommitSnapshotEffectsTimer();
if (enableProfilerTimer) {
recordCommitTime();
}
startCommitHostEffectsTimer();
stopCommitHostEffectsTimer();
startCommitLifeCyclesTimer();
stopCommitLifeCyclesTimer();
}
stopCommitTimer();
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;
pendingPassiveEffectsExpirationTime = expirationTime;
}
// Check if there's remaining work on this root
const remainingExpirationTime = root.firstPendingTime;
if (remainingExpirationTime !== NoWork) {
const currentTime = requestCurrentTime();
const priorityLevel = inferPriorityFromExpirationTime(
currentTime,
remainingExpirationTime,
);
if (enableSchedulerTracing) {
if (didDeprioritizeIdleSubtree) {
didDeprioritizeIdleSubtree = false;
scheduleInteractions(root, Never, root.memoizedInteractions);
}
}
scheduleCallbackForRoot(root, priorityLevel, remainingExpirationTime);
} 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, expirationTime);
}
}
onCommitRoot(finishedWork.stateNode, expirationTime);
if (remainingExpirationTime === Sync) {
// 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;
}
if (hasUncaughtError) {
hasUncaughtError = false;
const error = firstUncaughtError;
firstUncaughtError = null;
throw error;
}
if ((executionContext & LegacyUnbatchedContext) !== NoContext) {
// 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();
return null;
}
function commitBeforeMutationEffects() {
while (nextEffect !== null) {
if ((nextEffect.effectTag & Snapshot) !== NoEffect) {
setCurrentDebugFiberInDEV(nextEffect);
recordEffect();
const current = nextEffect.alternate;
commitBeforeMutationEffectOnFiber(current, nextEffect);
resetCurrentDebugFiberInDEV();
}
nextEffect = nextEffect.nextEffect;
}
}
function commitMutationEffects() {
// 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);
}
}
// 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.
let primaryEffectTag = effectTag & (Placement | Update | Deletion);
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 Update: {
const current = nextEffect.alternate;
commitWork(current, nextEffect);
break;
}
case Deletion: {
commitDeletion(nextEffect);
break;
}
}
// TODO: Only record a mutation effect if primaryEffectTag is non-zero.
recordEffect();
resetCurrentDebugFiberInDEV();
nextEffect = nextEffect.nextEffect;
}
}
function commitLayoutEffects(
root: FiberRoot,
committedExpirationTime: ExpirationTime,
) {
// TODO: Should probably move the bulk of this function to commitWork.
while (nextEffect !== null) {
setCurrentDebugFiberInDEV(nextEffect);
const effectTag = nextEffect.effectTag;
if (effectTag & (Update | Callback)) {
recordEffect();
const current = nextEffect.alternate;
commitLayoutEffectOnFiber(
root,
current,
nextEffect,
committedExpirationTime,
);
}
if (effectTag & Ref) {
recordEffect();
commitAttachRef(nextEffect);
}
if (effectTag & Passive) {
rootDoesHavePassiveEffects = true;
}
resetCurrentDebugFiberInDEV();
nextEffect = nextEffect.nextEffect;
}
}
export function flushPassiveEffects() {
if (rootWithPendingPassiveEffects === null) {
return false;
}
const root = rootWithPendingPassiveEffects;
const expirationTime = pendingPassiveEffectsExpirationTime;
rootWithPendingPassiveEffects = null;
pendingPassiveEffectsExpirationTime = NoWork;
let prevInteractions: Set<Interaction> | null = null;
if (enableSchedulerTracing) {
prevInteractions = __interactionsRef.current;
__interactionsRef.current = root.memoizedInteractions;
}
invariant(
(executionContext & (RenderContext | CommitContext)) === NoContext,
'Cannot flush passive effects while already rendering.',
);
const prevExecutionContext = executionContext;
executionContext |= CommitContext;
// 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) {
if (__DEV__) {
setCurrentDebugFiberInDEV(effect);
invokeGuardedCallback(null, commitPassiveHookEffects, null, effect);
if (hasCaughtError()) {
invariant(effect !== null, 'Should be working on an effect.');
const error = clearCaughtError();
captureCommitPhaseError(effect, error);
}
resetCurrentDebugFiberInDEV();
} else {
try {
commitPassiveHookEffects(effect);
} catch (error) {
invariant(effect !== null, 'Should be working on an effect.');
captureCommitPhaseError(effect, error);
}
}
effect = effect.nextEffect;
}
if (enableSchedulerTracing) {
__interactionsRef.current = ((prevInteractions: any): Set<Interaction>);
finishPendingInteractions(root, expirationTime);
}
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, Sync);
enqueueUpdate(rootFiber, update);
const root = markUpdateTimeFromFiberToRoot(rootFiber, Sync);
if (root !== null) {
scheduleCallbackForRoot(root, ImmediatePriority, Sync);
}
}
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,
// TODO: This is always sync
Sync,
);
enqueueUpdate(fiber, update);
const root = markUpdateTimeFromFiberToRoot(fiber, Sync);
if (root !== null) {
scheduleCallbackForRoot(root, ImmediatePriority, Sync);
}
return;
}
}
fiber = fiber.return;
}
}
export function pingSuspendedRoot(
root: FiberRoot,
thenable: Thenable,
suspendedTime: ExpirationTime,
) {
const pingCache = root.pingCache;
if (pingCache !== null) {
// The thenable resolved, so we no longer need to memoize, because it will
// never be thrown again.
pingCache.delete(thenable);
}
if (workInProgressRoot === root && renderExpirationTime === suspendedTime) {
// 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, we'll always suspend so we can always
// restart. If we're suspended without any updates, it might be a retry.
// If it's early in the retry we can restart. We can't know for sure
// whether we'll eventually process an update during this render pass,
// but it's somewhat unlikely that we get to a ping before that, since
// getting to the root most update is usually very fast.
if (
workInProgressRootExitStatus === RootSuspendedWithDelay ||
(workInProgressRootExitStatus === RootSuspended &&
workInProgressRootLatestProcessedExpirationTime === Sync &&
now() - globalMostRecentFallbackTime < FALLBACK_THROTTLE_MS)
) {
// Restart from the root. Don't need to schedule a ping because
// we're already working on this tree.
prepareFreshStack(root, renderExpirationTime);
} else {
// Even though we can't restart right now, we might get an
// opportunity later. So we mark this render as having a ping.
workInProgressRootHasPendingPing = true;
}
return;
}
const lastPendingTime = root.lastPendingTime;
if (lastPendingTime < suspendedTime) {
// The root is no longer suspended at this time.
return;
}
const pingTime = root.pingTime;
if (pingTime !== NoWork && pingTime < suspendedTime) {
// There's already a lower priority ping scheduled.
return;
}
// Mark the time at which this ping was scheduled.
root.pingTime = suspendedTime;
if (root.finishedExpirationTime === suspendedTime) {
// If there's a pending fallback waiting to commit, throw it away.
root.finishedExpirationTime = NoWork;
root.finishedWork = null;
}
const currentTime = requestCurrentTime();
const priorityLevel = inferPriorityFromExpirationTime(
currentTime,
suspendedTime,
);
scheduleCallbackForRoot(root, priorityLevel, suspendedTime);
}
export function retryTimedOutBoundary(boundaryFiber: Fiber) {
// The boundary fiber (a Suspense component) previously timed out and 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.
const currentTime = requestCurrentTime();
const suspenseConfig = null; // Retries don't carry over the already committed update.
const retryTime = computeExpirationForFiber(
currentTime,
boundaryFiber,
suspenseConfig,
);
// TODO: Special case idle priority?
const priorityLevel = inferPriorityFromExpirationTime(currentTime, retryTime);
const root = markUpdateTimeFromFiberToRoot(boundaryFiber, retryTime);
if (root !== null) {
scheduleCallbackForRoot(root, priorityLevel, retryTime);
}
}
export function resolveRetryThenable(boundaryFiber: Fiber, thenable: Thenable) {
let retryCache: WeakSet<Thenable> | Set<Thenable> | null;
if (enableSuspenseServerRenderer) {
switch (boundaryFiber.tag) {
case SuspenseComponent:
retryCache = boundaryFiber.stateNode;
break;
case DehydratedSuspenseComponent:
retryCache = boundaryFiber.memoizedState;
break;
default:
invariant(
false,
'Pinged unknown suspense boundary type. ' +
'This is probably a bug in React.',
);
}
} else {
retryCache = boundaryFiber.stateNode;
}
if (retryCache !== null) {
// The thenable resolved, so we no longer need to memoize, because it will
// never be thrown again.
retryCache.delete(thenable);
}
retryTimedOutBoundary(boundaryFiber);
}
// 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: ExpirationTime,
committedExpirationTime: ExpirationTime,
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 = inferTimeFromExpirationTimeWithSuspenseConfig(
mostRecentEventTime,
suspenseConfig,
);
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;
warning(
false,
'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.flushPendingUnsafeLifecycleWarnings();
ReactStrictModeWarnings.flushLegacyContextWarning();
if (warnAboutDeprecatedLifecycles) {
ReactStrictModeWarnings.flushPendingDeprecationWarnings();
}
}
}
function stopFinishedWorkLoopTimer() {
const didCompleteRoot = true;
stopWorkLoopTimer(interruptedBy, didCompleteRoot);
interruptedBy = null;
}
function stopInterruptedWorkLoopTimer() {
// TODO: Track which fiber caused the interruption.
const didCompleteRoot = false;
stopWorkLoopTimer(interruptedBy, didCompleteRoot);
interruptedBy = null;
}
function checkForInterruption(
fiberThatReceivedUpdate: Fiber,
updateExpirationTime: ExpirationTime,
) {
if (
enableUserTimingAPI &&
workInProgressRoot !== null &&
updateExpirationTime > renderExpirationTime
) {
interruptedBy = fiberThatReceivedUpdate;
}
}
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
) {
// 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 (didWarnStateUpdateForUnmountedComponent !== null) {
if (didWarnStateUpdateForUnmountedComponent.has(componentName)) {
return;
}
didWarnStateUpdateForUnmountedComponent.add(componentName);
} else {
didWarnStateUpdateForUnmountedComponent = new Set([componentName]);
}
warningWithoutStack(
false,
"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.%s',
tag === ClassComponent
? 'the componentWillUnmount method'
: 'a useEffect cleanup function',
getStackByFiberInDevAndProd(fiber),
);
}
}
let beginWork;
if (__DEV__ && replayFailedUnitOfWorkWithInvokeGuardedCallback) {
let dummyFiber = null;
beginWork = (current, unitOfWork, expirationTime) => {
// 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, expirationTime);
} 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 renderRoot; any changes here must have
// corresponding changes there.
resetContextDependencies();
resetHooks();
// 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,
expirationTime,
);
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 didWarnAboutUpdateInGetChildContext = false;
function warnAboutInvalidUpdatesOnClassComponentsInDEV(fiber) {
if (__DEV__) {
if (fiber.tag === ClassComponent) {
switch (ReactCurrentDebugFiberPhaseInDEV) {
case 'getChildContext':
if (didWarnAboutUpdateInGetChildContext) {
return;
}
warningWithoutStack(
false,
'setState(...): Cannot call setState() inside getChildContext()',
);
didWarnAboutUpdateInGetChildContext = true;
break;
case 'render':
if (didWarnAboutUpdateInRender) {
return;
}
warningWithoutStack(
false,
'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;
}
}
}
}
// We export a simple object here to be used by a renderer/test-utils
// as the value of ReactCurrentActingRendererSigil.current
// This identity lets us identify (ha!) when the wrong renderer's act()
// wraps anothers' updates/effects
export const ReactActingRendererSigil = {};
export function warnIfNotScopedWithMatchingAct(fiber: Fiber): void {
if (__DEV__) {
if (
ReactCurrentActingRendererSigil.current !== null &&
ReactCurrentActingRendererSigil.current !== ReactActingRendererSigil
) {
warningWithoutStack(
false,
"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' +
"import {act} from 'react-dom/test-utils';\n" +
'//...\n' +
'act(() => ...);\n\n' +
'// for react-test-renderer:\n' +
"import TestRenderer from 'react-test-renderer';\n" +
'const {act} = TestRenderer;\n' +
'//...\n' +
'act(() => ...);' +
'%s',
getStackByFiberInDevAndProd(fiber),
);
}
}
}
function warnIfNotCurrentlyActingUpdatesInDEV(fiber: Fiber): void {
if (__DEV__) {
if (
executionContext === NoContext &&
ReactCurrentActingRendererSigil.current !== ReactActingRendererSigil
) {
warningWithoutStack(
false,
'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' +
'%s',
getComponentName(fiber.type),
getStackByFiberInDevAndProd(fiber),
);
}
}
}
export const warnIfNotCurrentlyActingUpdatesInDev = warnIfNotCurrentlyActingUpdatesInDEV;
let componentsWithSuspendedDiscreteUpdates = null;
export function checkForWrongSuspensePriorityInDEV(sourceFiber: Fiber) {
if (__DEV__) {
if (
(sourceFiber.mode & ConcurrentMode) !== NoEffect &&
// Check if we're currently rendering a discrete update. Ideally, all we
// would need to do is check the current priority level. But we currently
// have no rigorous way to distinguish work that was scheduled at user-
// blocking priority from work that expired a bit and was "upgraded" to
// a higher priority. That's because we don't schedule separate callbacks
// for every level, only the highest priority level per root. The priority
// of subsequent levels is inferred from the expiration time, but this is
// an imprecise heuristic.
//
// However, we do store the last discrete pending update per root. So we
// can reliably compare to that one. (If we broaden this warning to include
// high pri updates that aren't discrete, then this won't be sufficient.)
//
// My rationale is that it's better for this warning to have false
// negatives than false positives.
rootsWithPendingDiscreteUpdates !== null &&
workInProgressRoot !== null &&
renderExpirationTime ===
rootsWithPendingDiscreteUpdates.get(workInProgressRoot)
) {
// Add the component name to a set.
const componentName = getComponentName(sourceFiber.type);
if (componentsWithSuspendedDiscreteUpdates === null) {
componentsWithSuspendedDiscreteUpdates = new Set([componentName]);
} else {
componentsWithSuspendedDiscreteUpdates.add(componentName);
}
}
}
}
function flushSuspensePriorityWarningInDEV() {
if (__DEV__) {
if (componentsWithSuspendedDiscreteUpdates !== null) {
const componentNames = [];
componentsWithSuspendedDiscreteUpdates.forEach(name => {
componentNames.push(name);
});
componentsWithSuspendedDiscreteUpdates = null;
// TODO: A more helpful version of this message could include the names of
// the component that were updated, not the ones that suspended. To do
// that we'd need to track all the components that updated during this
// render, perhaps using the same mechanism as `markRenderEventTime`.
warningWithoutStack(
false,
'The following components suspended during a user-blocking update: %s' +
'\n\n' +
'Updates triggered by user interactions (e.g. click events) are ' +
'considered user-blocking by default. They should not suspend. ' +
'Updates that can afford to take a bit longer should be wrapped ' +
'with `Scheduler.next` (or an equivalent abstraction). This ' +
'typically includes any update that shows new content, like ' +
'a navigation.' +
'\n\n' +
'Generally, you should split user interactions into at least two ' +
'seprate updates: a user-blocking update to provide immediate ' +
'feedback, and another update to perform the actual change.',
// TODO: Add link to React docs with more information, once it exists
componentNames.sort().join(', '),
);
}
}
}
function computeThreadID(root, expirationTime) {
// Interaction threads are unique per root and expiration time.
return expirationTime * 1000 + root.interactionThreadID;
}
export function markDidDeprioritizeIdleSubtree() {
if (!enableSchedulerTracing) {
return;
}
didDeprioritizeIdleSubtree = true;
}
function scheduleInteractions(root, expirationTime, interactions) {
if (!enableSchedulerTracing) {
return;
}
if (interactions.size > 0) {
const pendingInteractionMap = root.pendingInteractionMap;
const pendingInteractions = pendingInteractionMap.get(expirationTime);
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(expirationTime, 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, expirationTime);
subscriber.onWorkScheduled(interactions, threadID);
}
}
}
function schedulePendingInteractions(root, expirationTime) {
// 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, expirationTime, __interactionsRef.current);
}
function startWorkOnPendingInteractions(root, expirationTime) {
// 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, scheduledExpirationTime) => {
if (scheduledExpirationTime >= expirationTime) {
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 renderRoot() 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, expirationTime);
try {
subscriber.onWorkStarted(interactions, threadID);
} catch (error) {
// If the subscriber throws, rethrow it in a separate task
scheduleCallback(ImmediatePriority, () => {
throw error;
});
}
}
}
}
function finishPendingInteractions(root, committedExpirationTime) {
if (!enableSchedulerTracing) {
return;
}
const earliestRemainingTimeAfterCommit = root.firstPendingTime;
let subscriber;
try {
subscriber = __subscriberRef.current;
if (subscriber !== null && root.memoizedInteractions.size > 0) {
const threadID = computeThreadID(root, committedExpirationTime);
subscriber.onWorkStopped(root.memoizedInteractions, threadID);
}
} catch (error) {
// If the subscriber throws, rethrow it in a separate task
scheduleCallback(ImmediatePriority, () => {
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, scheduledExpirationTime) => {
// 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 (scheduledExpirationTime > earliestRemainingTimeAfterCommit) {
pendingInteractionMap.delete(scheduledExpirationTime);
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(ImmediatePriority, () => {
throw error;
});
}
}
});
}
},
);
}
}