diff --git a/packages/react-reconciler/src/ReactFiberWorkLoop.js b/packages/react-reconciler/src/ReactFiberWorkLoop.js
index 30b380de3f..aa3a38ba4a 100644
--- a/packages/react-reconciler/src/ReactFiberWorkLoop.js
+++ b/packages/react-reconciler/src/ReactFiberWorkLoop.js
@@ -176,14 +176,14 @@ const {
ReactCurrentActingRendererSigil,
} = ReactSharedInternals;
-type WorkPhase = 0 | 1 | 2 | 3 | 4 | 5 | 6;
-const NotWorking = 0;
-const BatchedPhase = 1;
-const LegacyUnbatchedPhase = 2;
-const FlushSyncPhase = 3;
-const RenderPhase = 4;
-const CommitPhase = 5;
-const BatchedEventPhase = 6;
+type ExecutionContext = number;
+
+const NoContext = /* */ 0b00000;
+const BatchedContext = /* */ 0b00001;
+const EventContext = /* */ 0b00010;
+const LegacyUnbatchedContext = /* */ 0b00100;
+const RenderContext = /* */ 0b01000;
+const CommitContext = /* */ 0b10000;
type RootExitStatus = 0 | 1 | 2 | 3 | 4;
const RootIncomplete = 0;
@@ -196,8 +196,8 @@ export type Thenable = {
then(resolve: () => mixed, reject?: () => mixed): Thenable | void,
};
-// The phase of work we're currently in
-let workPhase: WorkPhase = NotWorking;
+// 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
@@ -257,7 +257,7 @@ let interruptedBy: Fiber | null = null;
let currentEventTime: ExpirationTime = NoWork;
export function requestCurrentTime() {
- if (workPhase === RenderPhase || workPhase === CommitPhase) {
+ if ((executionContext & (RenderContext | CommitContext)) !== NoContext) {
// We're inside React, so it's fine to read the actual time.
return msToExpirationTime(now());
}
@@ -286,7 +286,7 @@ export function computeExpirationForFiber(
return priorityLevel === ImmediatePriority ? Sync : Batched;
}
- if (workPhase === RenderPhase) {
+ if ((executionContext & RenderContext) !== NoContext) {
// Use whatever time we're already rendering
return renderExpirationTime;
}
@@ -364,7 +364,12 @@ export function scheduleUpdateOnFiber(
recordScheduleUpdate();
if (expirationTime === Sync) {
- if (workPhase === LegacyUnbatchedPhase) {
+ 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.
schedulePendingInteraction(root, expirationTime);
@@ -377,7 +382,7 @@ export function scheduleUpdateOnFiber(
}
} else {
scheduleCallbackForRoot(root, ImmediatePriority, Sync);
- if (workPhase === NotWorking) {
+ 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
@@ -518,8 +523,7 @@ function scheduleCallbackForRoot(
if (
enableUserTimingAPI &&
expirationTime !== Sync &&
- workPhase !== RenderPhase &&
- workPhase !== CommitPhase
+ (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
@@ -557,7 +561,7 @@ function runRootCallback(root, callback, isSync) {
}
export function flushRoot(root: FiberRoot, expirationTime: ExpirationTime) {
- if (workPhase === RenderPhase || workPhase === CommitPhase) {
+ if ((executionContext & (RenderContext | CommitContext)) !== NoContext) {
invariant(
false,
'work.commit(): Cannot commit while already rendering. This likely ' +
@@ -569,22 +573,24 @@ export function flushRoot(root: FiberRoot, expirationTime: ExpirationTime) {
}
export function flushDiscreteUpdates() {
- // TODO: we ideally do not want to early reurn for BatchedPhase here either.
- // Removing this causes act() tests to fail, so we should follow up.
- if (workPhase === CommitPhase || workPhase === BatchedPhase) {
- // We're inside the commit phase or batched phase, 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;
- }
- if (workPhase === RenderPhase) {
- if (__DEV__) {
+ // 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();
@@ -650,69 +656,62 @@ function flushPendingDiscreteUpdates() {
}
export function batchedUpdates(fn: A => R, a: A): R {
- if (workPhase !== NotWorking) {
- // We're already working, or inside a batch, so batchedUpdates is a no-op.
- return fn(a);
- }
- workPhase = BatchedPhase;
+ const prevExecutionContext = executionContext;
+ executionContext |= BatchedContext;
try {
return fn(a);
} finally {
- workPhase = NotWorking;
- // Flush the immediate callbacks that were scheduled during this batch
- flushSyncCallbackQueue();
+ executionContext = prevExecutionContext;
+ if (executionContext === NoContext) {
+ // Flush the immediate callbacks that were scheduled during this batch
+ flushSyncCallbackQueue();
+ }
}
}
export function batchedEventUpdates(fn: A => R, a: A): R {
- if (workPhase !== NotWorking) {
- // We're already working, or inside a batch, so batchedUpdates is a no-op.
- return fn(a);
- }
- const prevWorkPhase = workPhase;
- workPhase = BatchedEventPhase;
+ const prevExecutionContext = executionContext;
+ executionContext |= EventContext;
try {
return fn(a);
} finally {
- workPhase = prevWorkPhase;
- // Flush the immediate callbacks that were scheduled during this batch
- flushSyncCallbackQueue();
+ executionContext = prevExecutionContext;
+ if (executionContext === NoContext) {
+ // Flush the immediate callbacks that were scheduled during this batch
+ flushSyncCallbackQueue();
+ }
}
}
export function unbatchedUpdates(fn: (a: A) => R, a: A): R {
- if (
- workPhase !== BatchedPhase &&
- workPhase !== FlushSyncPhase &&
- workPhase !== BatchedEventPhase
- ) {
- // We're not inside batchedUpdates or flushSync, so unbatchedUpdates is
- // a no-op.
- return fn(a);
- }
- const prevWorkPhase = workPhase;
- workPhase = LegacyUnbatchedPhase;
+ const prevExecutionContext = executionContext;
+ executionContext &= ~BatchedContext;
+ executionContext |= LegacyUnbatchedContext;
try {
return fn(a);
} finally {
- workPhase = prevWorkPhase;
+ executionContext = prevExecutionContext;
+ if (executionContext === NoContext) {
+ // Flush the immediate callbacks that were scheduled during this batch
+ flushSyncCallbackQueue();
+ }
}
}
export function flushSync(fn: A => R, a: A): R {
- if (workPhase === RenderPhase || workPhase === CommitPhase) {
+ 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 prevWorkPhase = workPhase;
- workPhase = FlushSyncPhase;
+ const prevExecutionContext = executionContext;
+ executionContext |= BatchedContext;
try {
return runWithPriority(ImmediatePriority, fn.bind(null, a));
} finally {
- workPhase = prevWorkPhase;
+ 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.
@@ -721,13 +720,13 @@ export function flushSync(fn: A => R, a: A): R {
}
export function flushControlled(fn: () => mixed): void {
- const prevWorkPhase = workPhase;
- workPhase = BatchedPhase;
+ const prevExecutionContext = executionContext;
+ executionContext |= BatchedContext;
try {
runWithPriority(ImmediatePriority, fn);
} finally {
- workPhase = prevWorkPhase;
- if (workPhase === NotWorking) {
+ executionContext = prevExecutionContext;
+ if (executionContext === NoContext) {
// Flush the immediate callbacks that were scheduled during this batch
flushSyncCallbackQueue();
}
@@ -775,7 +774,7 @@ function renderRoot(
isSync: boolean,
): SchedulerCallback | null {
invariant(
- workPhase !== RenderPhase && workPhase !== CommitPhase,
+ (executionContext & (RenderContext | CommitContext)) === NoContext,
'Should not already be working.',
);
@@ -828,8 +827,8 @@ function renderRoot(
// If we have a work-in-progress fiber, it means there's still work to do
// in this root.
if (workInProgress !== null) {
- const prevWorkPhase = workPhase;
- workPhase = RenderPhase;
+ const prevExecutionContext = executionContext;
+ executionContext |= RenderContext;
let prevDispatcher = ReactCurrentDispatcher.current;
if (prevDispatcher === null) {
// The React isomorphic package does not include a default dispatcher.
@@ -855,7 +854,7 @@ function renderRoot(
const currentTime = requestCurrentTime();
if (currentTime < expirationTime) {
// Restart at the current time.
- workPhase = prevWorkPhase;
+ executionContext = prevExecutionContext;
resetContextDependencies();
ReactCurrentDispatcher.current = prevDispatcher;
if (enableSchedulerTracing) {
@@ -892,7 +891,7 @@ function renderRoot(
// supposed to capture all errors that weren't caught by an error
// boundary.
prepareFreshStack(root, expirationTime);
- workPhase = prevWorkPhase;
+ executionContext = prevExecutionContext;
throw thrownValue;
}
@@ -915,7 +914,7 @@ function renderRoot(
}
} while (true);
- workPhase = prevWorkPhase;
+ executionContext = prevExecutionContext;
resetContextDependencies();
ReactCurrentDispatcher.current = prevDispatcher;
if (enableSchedulerTracing) {
@@ -1454,7 +1453,7 @@ function commitRootImpl(root) {
flushSuspensePriorityWarningInDEV();
invariant(
- workPhase !== RenderPhase && workPhase !== CommitPhase,
+ (executionContext & (RenderContext | CommitContext)) === NoContext,
'Should not already be working.',
);
@@ -1524,8 +1523,8 @@ function commitRootImpl(root) {
}
if (firstEffect !== null) {
- const prevWorkPhase = workPhase;
- workPhase = CommitPhase;
+ const prevExecutionContext = executionContext;
+ executionContext |= CommitContext;
let prevInteractions: Set | null = null;
if (enableSchedulerTracing) {
prevInteractions = __interactionsRef.current;
@@ -1640,7 +1639,7 @@ function commitRootImpl(root) {
if (enableSchedulerTracing) {
__interactionsRef.current = ((prevInteractions: any): Set);
}
- workPhase = prevWorkPhase;
+ executionContext = prevExecutionContext;
} else {
// No effects.
root.current = finishedWork;
@@ -1712,7 +1711,7 @@ function commitRootImpl(root) {
throw error;
}
- if (workPhase === LegacyUnbatchedPhase) {
+ 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
@@ -1855,11 +1854,11 @@ export function flushPassiveEffects() {
}
invariant(
- workPhase !== RenderPhase && workPhase !== CommitPhase,
+ (executionContext & (RenderContext | CommitContext)) === NoContext,
'Cannot flush passive effects while already rendering.',
);
- const prevWorkPhase = workPhase;
- workPhase = CommitPhase;
+ 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
@@ -1891,7 +1890,7 @@ export function flushPassiveEffects() {
finishPendingInteractions(root, expirationTime);
}
- workPhase = prevWorkPhase;
+ executionContext = prevExecutionContext;
flushSyncCallbackQueue();
// If additional passive effects were scheduled, increment a counter. If this
@@ -2400,7 +2399,7 @@ export function warnIfNotScopedWithMatchingAct(fiber: Fiber): void {
function warnIfNotCurrentlyActingUpdatesInDEV(fiber: Fiber): void {
if (__DEV__) {
if (
- workPhase === NotWorking &&
+ executionContext === NoContext &&
ReactCurrentActingRendererSigil.current !== ReactActingRendererSigil
) {
warningWithoutStack(