From d8dfc22674cda6e7dc14617dfe63045471941982 Mon Sep 17 00:00:00 2001 From: Jan Kassens Date: Mon, 17 Apr 2023 16:24:41 -0400 Subject: [PATCH] Revert "Move update scheduling to microtask (#26512)" This reverts commit 09c8d2563300621dc91258a4c2839210e2fbdf0e. --- .../src/__tests__/profilingCache-test.js | 2 +- .../react-reconciler/src/ReactFiberRoot.js | 1 - .../src/ReactFiberRootScheduler.js | 464 ------------------ .../src/ReactFiberSyncTaskQueue.js | 118 +++++ .../src/ReactFiberWorkLoop.js | 278 +++++++++-- .../src/ReactInternalTypes.js | 4 - .../ReactFlushSyncNoAggregateError-test.js | 13 +- .../__tests__/ReactIncrementalUpdates-test.js | 15 +- .../__tests__/ReactProfiler-test.internal.js | 23 +- packages/shared/ReactFeatureFlags.js | 4 - .../ReactFeatureFlags.native-fb-dynamic.js | 1 - .../forks/ReactFeatureFlags.native-fb.js | 3 +- .../forks/ReactFeatureFlags.native-oss.js | 1 - .../forks/ReactFeatureFlags.test-renderer.js | 1 - .../ReactFeatureFlags.test-renderer.native.js | 1 - .../ReactFeatureFlags.test-renderer.www.js | 1 - .../forks/ReactFeatureFlags.www-dynamic.js | 1 - .../shared/forks/ReactFeatureFlags.www.js | 1 - scripts/flow/xplat.js | 1 - 19 files changed, 376 insertions(+), 557 deletions(-) delete mode 100644 packages/react-reconciler/src/ReactFiberRootScheduler.js create mode 100644 packages/react-reconciler/src/ReactFiberSyncTaskQueue.js diff --git a/packages/react-devtools-shared/src/__tests__/profilingCache-test.js b/packages/react-devtools-shared/src/__tests__/profilingCache-test.js index a4f8613e61546..d8d0690aa9c2a 100644 --- a/packages/react-devtools-shared/src/__tests__/profilingCache-test.js +++ b/packages/react-devtools-shared/src/__tests__/profilingCache-test.js @@ -963,7 +963,7 @@ describe('ProfilingCache', () => { 2 => 0, }, "passiveEffectDuration": null, - "priorityLevel": "Normal", + "priorityLevel": "Immediate", "timestamp": 0, "updaters": [ { diff --git a/packages/react-reconciler/src/ReactFiberRoot.js b/packages/react-reconciler/src/ReactFiberRoot.js index 2733d0ccf3515..c8df0a9e14673 100644 --- a/packages/react-reconciler/src/ReactFiberRoot.js +++ b/packages/react-reconciler/src/ReactFiberRoot.js @@ -63,7 +63,6 @@ function FiberRootNode( this.cancelPendingCommit = null; this.context = null; this.pendingContext = null; - this.next = null; this.callbackNode = null; this.callbackPriority = NoLane; this.eventTimes = createLaneMap(NoLanes); diff --git a/packages/react-reconciler/src/ReactFiberRootScheduler.js b/packages/react-reconciler/src/ReactFiberRootScheduler.js deleted file mode 100644 index 5068194aa24cc..0000000000000 --- a/packages/react-reconciler/src/ReactFiberRootScheduler.js +++ /dev/null @@ -1,464 +0,0 @@ -/** - * Copyright (c) Meta Platforms, Inc. and 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 {FiberRoot} from './ReactInternalTypes'; -import type {Lane} from './ReactFiberLane'; -import type {PriorityLevel} from 'scheduler/src/SchedulerPriorities'; - -import {enableDeferRootSchedulingToMicrotask} from 'shared/ReactFeatureFlags'; -import { - NoLane, - NoLanes, - SyncLane, - getHighestPriorityLane, - getNextLanes, - includesOnlyNonUrgentLanes, - includesSyncLane, - markStarvedLanesAsExpired, -} from './ReactFiberLane'; -import { - CommitContext, - NoContext, - RenderContext, - getExecutionContext, - getWorkInProgressRoot, - getWorkInProgressRootRenderLanes, - isWorkLoopSuspendedOnData, - performConcurrentWorkOnRoot, - performSyncWorkOnRoot, -} from './ReactFiberWorkLoop'; -import {LegacyRoot} from './ReactRootTags'; -import { - ImmediatePriority as ImmediateSchedulerPriority, - UserBlockingPriority as UserBlockingSchedulerPriority, - NormalPriority as NormalSchedulerPriority, - IdlePriority as IdleSchedulerPriority, - cancelCallback as Scheduler_cancelCallback, - scheduleCallback as Scheduler_scheduleCallback, - now, -} from './Scheduler'; -import { - DiscreteEventPriority, - ContinuousEventPriority, - DefaultEventPriority, - IdleEventPriority, - lanesToEventPriority, -} from './ReactEventPriorities'; -import {supportsMicrotasks, scheduleMicrotask} from './ReactFiberHostConfig'; - -import ReactSharedInternals from 'shared/ReactSharedInternals'; -const {ReactCurrentActQueue} = ReactSharedInternals; - -// A linked list of all the roots with pending work. In an idiomatic app, -// there's only a single root, but we do support multi root apps, hence this -// extra complexity. But this module is optimized for the single root case. -let firstScheduledRoot: FiberRoot | null = null; -let lastScheduledRoot: FiberRoot | null = null; - -// Used to prevent redundant mircotasks from being scheduled. -let didScheduleMicrotask: boolean = false; -// `act` "microtasks" are scheduled on the `act` queue instead of an actual -// microtask, so we have to dedupe those separately. This wouldn't be an issue -// if we required all `act` calls to be awaited, which we might in the future. -let didScheduleMicrotask_act: boolean = false; - -// Used to quickly bail out of flushSync if there's no sync work to do. -let mightHavePendingSyncWork: boolean = false; - -let isFlushingWork: boolean = false; - -export function ensureRootIsScheduled(root: FiberRoot): void { - // This function is called whenever a root receives an update. It does two - // things 1) it ensures the root is in the root schedule, and 2) it ensures - // there's a pending microtask to process the root schedule. - // - // Most of the actual scheduling logic does not happen until - // `scheduleTaskForRootDuringMicrotask` runs. - - // Add the root to the schedule - if (root === lastScheduledRoot || root.next !== null) { - // Fast path. This root is already scheduled. - } else { - if (lastScheduledRoot === null) { - firstScheduledRoot = lastScheduledRoot = root; - } else { - lastScheduledRoot.next = root; - lastScheduledRoot = root; - } - } - - // Any time a root received an update, we set this to true until the next time - // we process the schedule. If it's false, then we can quickly exit flushSync - // without consulting the schedule. - mightHavePendingSyncWork = true; - - // At the end of the current event, go through each of the roots and ensure - // there's a task scheduled for each one at the correct priority. - if (__DEV__ && ReactCurrentActQueue.current !== null) { - // We're inside an `act` scope. - if (!didScheduleMicrotask_act) { - didScheduleMicrotask_act = true; - scheduleImmediateTask(processRootScheduleInMicrotask); - } - } else { - if (!didScheduleMicrotask) { - didScheduleMicrotask = true; - scheduleImmediateTask(processRootScheduleInMicrotask); - } - } - - if (!enableDeferRootSchedulingToMicrotask) { - // While this flag is disabled, we schedule the render task immediately - // instead of waiting a microtask. - // TODO: We need to land enableDeferRootSchedulingToMicrotask ASAP to - // unblock additional features we have planned. - scheduleTaskForRootDuringMicrotask(root, now()); - } -} - -export function flushSyncWorkOnAllRoots() { - // This is allowed to be called synchronously, but the caller should check - // the execution context first. - flushSyncWorkAcrossRoots_impl(false); -} - -export function flushSyncWorkOnLegacyRootsOnly() { - // This is allowed to be called synchronously, but the caller should check - // the execution context first. - flushSyncWorkAcrossRoots_impl(true); -} - -function flushSyncWorkAcrossRoots_impl(onlyLegacy: boolean) { - if (isFlushingWork) { - // Prevent reentrancy. - // TODO: Is this overly defensive? The callers must check the execution - // context first regardless. - return; - } - - if (!mightHavePendingSyncWork) { - // Fast path. There's no sync work to do. - return; - } - - const workInProgressRoot = getWorkInProgressRoot(); - const workInProgressRootRenderLanes = getWorkInProgressRootRenderLanes(); - - // There may or may not be synchronous work scheduled. Let's check. - let didPerformSomeWork; - let errors: Array | null = null; - isFlushingWork = true; - do { - didPerformSomeWork = false; - let root = firstScheduledRoot; - while (root !== null) { - if (onlyLegacy && root.tag !== LegacyRoot) { - // Skip non-legacy roots. - } else { - const nextLanes = getNextLanes( - root, - root === workInProgressRoot ? workInProgressRootRenderLanes : NoLanes, - ); - if (includesSyncLane(nextLanes)) { - // This root has pending sync work. Flush it now. - try { - // TODO: Pass nextLanes as an argument instead of computing it again - // inside performSyncWorkOnRoot. - didPerformSomeWork = true; - performSyncWorkOnRoot(root); - } catch (error) { - // Collect errors so we can rethrow them at the end - if (errors === null) { - errors = [error]; - } else { - errors.push(error); - } - } - } - } - root = root.next; - } - } while (didPerformSomeWork); - isFlushingWork = false; - - // If any errors were thrown, rethrow them right before exiting. - // TODO: Consider returning these to the caller, to allow them to decide - // how/when to rethrow. - if (errors !== null) { - if (errors.length > 1) { - if (typeof AggregateError === 'function') { - // eslint-disable-next-line no-undef - throw new AggregateError(errors); - } else { - for (let i = 1; i < errors.length; i++) { - scheduleImmediateTask(throwError.bind(null, errors[i])); - } - const firstError = errors[0]; - throw firstError; - } - } else { - const error = errors[0]; - throw error; - } - } -} - -function throwError(error: mixed) { - throw error; -} - -function processRootScheduleInMicrotask() { - // This function is always called inside a microtask. It should never be - // called synchronously. - didScheduleMicrotask = false; - if (__DEV__) { - didScheduleMicrotask_act = false; - } - - // We'll recompute this as we iterate through all the roots and schedule them. - mightHavePendingSyncWork = false; - - const currentTime = now(); - - let prev = null; - let root = firstScheduledRoot; - while (root !== null) { - const next = root.next; - const nextLanes = scheduleTaskForRootDuringMicrotask(root, currentTime); - if (nextLanes === NoLane) { - // This root has no more pending work. Remove it from the schedule. To - // guard against subtle reentrancy bugs, this microtask is the only place - // we do this — you can add roots to the schedule whenever, but you can - // only remove them here. - - // Null this out so we know it's been removed from the schedule. - root.next = null; - if (prev === null) { - // This is the new head of the list - firstScheduledRoot = next; - } else { - prev.next = next; - } - if (next === null) { - // This is the new tail of the list - lastScheduledRoot = prev; - } - } else { - // This root still has work. Keep it in the list. - prev = root; - if (includesSyncLane(nextLanes)) { - mightHavePendingSyncWork = true; - } - } - root = next; - } - - // At the end of the microtask, flush any pending synchronous work. This has - // to come at the end, because it does actual rendering work that might throw. - flushSyncWorkOnAllRoots(); -} - -function scheduleTaskForRootDuringMicrotask( - root: FiberRoot, - currentTime: number, -): Lane { - // This function is always called inside a microtask, or at the very end of a - // rendering task right before we yield to the main thread. It should never be - // called synchronously. - // - // TODO: Unless enableDeferRootSchedulingToMicrotask is off. We need to land - // that ASAP to unblock additional features we have planned. - // - // This function also never performs React work synchronously; it should - // only schedule work to be performed later, in a separate task or microtask. - - // Check if any lanes are being starved by other work. If so, mark them as - // expired so we know to work on those next. - markStarvedLanesAsExpired(root, currentTime); - - // Determine the next lanes to work on, and their priority. - const workInProgressRoot = getWorkInProgressRoot(); - const workInProgressRootRenderLanes = getWorkInProgressRootRenderLanes(); - const nextLanes = getNextLanes( - root, - root === workInProgressRoot ? workInProgressRootRenderLanes : NoLanes, - ); - - const existingCallbackNode = root.callbackNode; - if ( - nextLanes === NoLanes || - // If this root is currently suspended and waiting for data to resolve, don't - // schedule a task to render it. We'll either wait for a ping, or wait to - // receive an update. - (isWorkLoopSuspendedOnData() && root === workInProgressRoot) || - // We should only interrupt a pending commit if the new update - // is urgent. - (root.cancelPendingCommit !== null && includesOnlyNonUrgentLanes(nextLanes)) - ) { - // Fast path: There's nothing to work on. - if (existingCallbackNode !== null) { - cancelCallback(existingCallbackNode); - } - root.callbackNode = null; - root.callbackPriority = NoLane; - return NoLane; - } - - // Schedule a new callback in the host environment. - if (includesSyncLane(nextLanes)) { - // Synchronous work is always flushed at the end of the microtask, so we - // don't need to schedule an additional task. - if (existingCallbackNode !== null) { - cancelCallback(existingCallbackNode); - } - root.callbackPriority = SyncLane; - root.callbackNode = null; - return SyncLane; - } else { - // We use the highest priority lane to represent the priority of the callback. - const existingCallbackPriority = root.callbackPriority; - const newCallbackPriority = getHighestPriorityLane(nextLanes); - - if ( - newCallbackPriority === existingCallbackPriority && - // Special case related to `act`. If the currently scheduled task is a - // Scheduler task, rather than an `act` task, cancel it and re-schedule - // on the `act` queue. - !( - __DEV__ && - ReactCurrentActQueue.current !== null && - existingCallbackNode !== fakeActCallbackNode - ) - ) { - // The priority hasn't changed. We can reuse the existing task. - return newCallbackPriority; - } else { - // Cancel the existing callback. We'll schedule a new one below. - cancelCallback(existingCallbackNode); - } - - let schedulerPriorityLevel; - switch (lanesToEventPriority(nextLanes)) { - case DiscreteEventPriority: - schedulerPriorityLevel = ImmediateSchedulerPriority; - break; - case ContinuousEventPriority: - schedulerPriorityLevel = UserBlockingSchedulerPriority; - break; - case DefaultEventPriority: - schedulerPriorityLevel = NormalSchedulerPriority; - break; - case IdleEventPriority: - schedulerPriorityLevel = IdleSchedulerPriority; - break; - default: - schedulerPriorityLevel = NormalSchedulerPriority; - break; - } - - const newCallbackNode = scheduleCallback( - schedulerPriorityLevel, - performConcurrentWorkOnRoot.bind(null, root), - ); - - root.callbackPriority = newCallbackPriority; - root.callbackNode = newCallbackNode; - return newCallbackPriority; - } -} - -export type RenderTaskFn = (didTimeout: boolean) => RenderTaskFn | null; - -export function getContinuationForRoot( - root: FiberRoot, - originalCallbackNode: mixed, -): RenderTaskFn | null { - // This is called at the end of `performConcurrentWorkOnRoot` to determine - // if we need to schedule a continuation task. - // - // Usually `scheduleTaskForRootDuringMicrotask` only runs inside a microtask; - // however, since most of the logic for determining if we need a continuation - // versus a new task is the same, we cheat a bit and call it here. This is - // only safe to do because we know we're at the end of the browser task. - // So although it's not an actual microtask, it might as well be. - scheduleTaskForRootDuringMicrotask(root, now()); - if (root.callbackNode === originalCallbackNode) { - // The task node scheduled for this root is the same one that's - // currently executed. Need to return a continuation. - return performConcurrentWorkOnRoot.bind(null, root); - } - return null; -} - -const fakeActCallbackNode = {}; - -function scheduleCallback( - priorityLevel: PriorityLevel, - callback: RenderTaskFn, -) { - if (__DEV__ && ReactCurrentActQueue.current !== null) { - // Special case: We're inside an `act` scope (a testing utility). - // Instead of scheduling work in the host environment, add it to a - // fake internal queue that's managed by the `act` implementation. - ReactCurrentActQueue.current.push(callback); - return fakeActCallbackNode; - } else { - return Scheduler_scheduleCallback(priorityLevel, callback); - } -} - -function cancelCallback(callbackNode: mixed) { - if (__DEV__ && callbackNode === fakeActCallbackNode) { - // Special `act` case: check if this is the fake callback node used by - // the `act` implementation. - } else if (callbackNode !== null) { - Scheduler_cancelCallback(callbackNode); - } -} - -function scheduleImmediateTask(cb: () => mixed) { - if (__DEV__ && ReactCurrentActQueue.current !== null) { - // Special case: Inside an `act` scope, we push microtasks to the fake `act` - // callback queue. This is because we currently support calling `act` - // without awaiting the result. The plan is to deprecate that, and require - // that you always await the result so that the microtasks have a chance to - // run. But it hasn't happened yet. - ReactCurrentActQueue.current.push(() => { - cb(); - return null; - }); - } - - // TODO: Can we land supportsMicrotasks? Which environments don't support it? - // Alternatively, can we move this check to the host config? - if (supportsMicrotasks) { - scheduleMicrotask(() => { - // In Safari, appending an iframe forces microtasks to run. - // https://github.com/facebook/react/issues/22459 - // We don't support running callbacks in the middle of render - // or commit so we need to check against that. - const executionContext = getExecutionContext(); - if ((executionContext & (RenderContext | CommitContext)) !== NoContext) { - // Note that this would still prematurely flush the callbacks - // if this happens outside render or commit phase (e.g. in an event). - - // Intentionally using a macrotask instead of a microtask here. This is - // wrong semantically but it prevents an infinite loop. The bug is - // Safari's, not ours, so we just do our best to not crash even though - // the behavior isn't completely correct. - Scheduler_scheduleCallback(ImmediateSchedulerPriority, cb); - return; - } - cb(); - }); - } else { - // If microtasks are not supported, use Scheduler. - Scheduler_scheduleCallback(ImmediateSchedulerPriority, cb); - } -} diff --git a/packages/react-reconciler/src/ReactFiberSyncTaskQueue.js b/packages/react-reconciler/src/ReactFiberSyncTaskQueue.js new file mode 100644 index 0000000000000..0b11be38a1dac --- /dev/null +++ b/packages/react-reconciler/src/ReactFiberSyncTaskQueue.js @@ -0,0 +1,118 @@ +/** + * Copyright (c) Meta Platforms, Inc. and 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 {SchedulerCallback} from './Scheduler'; + +import { + DiscreteEventPriority, + getCurrentUpdatePriority, + setCurrentUpdatePriority, +} from './ReactEventPriorities'; +import {ImmediatePriority, scheduleCallback} from './Scheduler'; + +let syncQueue: Array | null = null; +let includesLegacySyncCallbacks: boolean = false; +let isFlushingSyncQueue: boolean = false; + +export function scheduleSyncCallback(callback: SchedulerCallback) { + // Push this callback into an internal queue. We'll flush these either in + // the next tick, or earlier if something calls `flushSyncCallbackQueue`. + if (syncQueue === null) { + syncQueue = [callback]; + } else { + // Push onto existing queue. Don't need to schedule a callback because + // we already scheduled one when we created the queue. + syncQueue.push(callback); + } +} + +export function scheduleLegacySyncCallback(callback: SchedulerCallback) { + includesLegacySyncCallbacks = true; + scheduleSyncCallback(callback); +} + +export function flushSyncCallbacksOnlyInLegacyMode() { + // Only flushes the queue if there's a legacy sync callback scheduled. + // TODO: There's only a single type of callback: performSyncOnWorkOnRoot. So + // it might make more sense for the queue to be a list of roots instead of a + // list of generic callbacks. Then we can have two: one for legacy roots, one + // for concurrent roots. And this method would only flush the legacy ones. + if (includesLegacySyncCallbacks) { + flushSyncCallbacks(); + } +} + +export function flushSyncCallbacks(): null { + if (!isFlushingSyncQueue && syncQueue !== null) { + // Prevent re-entrance. + isFlushingSyncQueue = true; + + // Set the event priority to discrete + // TODO: Is this necessary anymore? The only user code that runs in this + // queue is in the render or commit phases, which already set the + // event priority. Should be able to remove. + const previousUpdatePriority = getCurrentUpdatePriority(); + setCurrentUpdatePriority(DiscreteEventPriority); + + let errors: Array | null = null; + + const queue = syncQueue; + // $FlowFixMe[incompatible-use] found when upgrading Flow + for (let i = 0; i < queue.length; i++) { + // $FlowFixMe[incompatible-use] found when upgrading Flow + let callback: SchedulerCallback = queue[i]; + try { + do { + const isSync = true; + // $FlowFixMe[incompatible-type] we bail out when we get a null + callback = callback(isSync); + } while (callback !== null); + } catch (error) { + // Collect errors so we can rethrow them at the end + if (errors === null) { + errors = [error]; + } else { + errors.push(error); + } + } + } + + syncQueue = null; + includesLegacySyncCallbacks = false; + setCurrentUpdatePriority(previousUpdatePriority); + isFlushingSyncQueue = false; + + if (errors !== null) { + if (errors.length > 1) { + if (typeof AggregateError === 'function') { + // eslint-disable-next-line no-undef + throw new AggregateError(errors); + } else { + for (let i = 1; i < errors.length; i++) { + scheduleCallback( + ImmediatePriority, + throwError.bind(null, errors[i]), + ); + } + const firstError = errors[0]; + throw firstError; + } + } else { + const error = errors[0]; + throw error; + } + } + } + + return null; +} + +function throwError(error: mixed) { + throw error; +} diff --git a/packages/react-reconciler/src/ReactFiberWorkLoop.js b/packages/react-reconciler/src/ReactFiberWorkLoop.js index 55c30ae81a896..f1810f8a785c1 100644 --- a/packages/react-reconciler/src/ReactFiberWorkLoop.js +++ b/packages/react-reconciler/src/ReactFiberWorkLoop.js @@ -22,7 +22,6 @@ import type { TransitionAbort, } from './ReactFiberTracingMarkerComponent'; import type {OffscreenInstance} from './ReactFiberOffscreenComponent'; -import type {RenderTaskFn} from './ReactFiberRootScheduler'; import { replayFailedUnitOfWorkWithInvokeGuardedCallback, @@ -46,12 +45,21 @@ import is from 'shared/objectIs'; import { // Aliased because `act` will override and push to an internal queue scheduleCallback as Scheduler_scheduleCallback, + cancelCallback as Scheduler_cancelCallback, shouldYield, requestPaint, now, + ImmediatePriority as ImmediateSchedulerPriority, + UserBlockingPriority as UserBlockingSchedulerPriority, NormalPriority as NormalSchedulerPriority, IdlePriority as IdleSchedulerPriority, } from './Scheduler'; +import { + flushSyncCallbacks, + flushSyncCallbacksOnlyInLegacyMode, + scheduleSyncCallback, + scheduleLegacySyncCallback, +} from './ReactFiberSyncTaskQueue'; import { logCommitStarted, logCommitStopped, @@ -70,7 +78,9 @@ import { noTimeout, afterActiveInstanceBlur, getCurrentEventPriority, + supportsMicrotasks, errorHydratingContainer, + scheduleMicrotask, prepareRendererToRender, resetRendererAfterRender, startSuspendingCommit, @@ -143,6 +153,7 @@ import { includesBlockingLane, includesExpiredLane, getNextLanes, + markStarvedLanesAsExpired, getLanesToRetrySynchronouslyOnError, getMostRecentEventTime, markRootUpdated, @@ -150,6 +161,7 @@ import { markRootPinged, markRootEntangled, markRootFinished, + getHighestPriorityLane, addFiberToLanesMap, movePendingFibersToMemoized, addTransitionToLanesMap, @@ -160,7 +172,9 @@ import { } from './ReactFiberLane'; import { DiscreteEventPriority, + ContinuousEventPriority, DefaultEventPriority, + IdleEventPriority, getCurrentUpdatePriority, setCurrentUpdatePriority, lowerEventPriority, @@ -275,12 +289,6 @@ import { } from './ReactFiberSuspenseContext'; import {resolveDefaultProps} from './ReactFiberLazyComponent'; import {resetChildReconcilerOnUnwind} from './ReactChildFiber'; -import { - ensureRootIsScheduled, - flushSyncWorkOnAllRoots, - flushSyncWorkOnLegacyRootsOnly, - getContinuationForRoot, -} from './ReactFiberRootScheduler'; const ceil = Math.ceil; @@ -601,10 +609,6 @@ export function getWorkInProgressRootRenderLanes(): Lanes { return workInProgressRootRenderLanes; } -export function isWorkLoopSuspendedOnData(): boolean { - return workInProgressSuspendedReason === SuspendedOnData; -} - export function requestEventTime(): number { if ((executionContext & (RenderContext | CommitContext)) !== NoContext) { // We're inside React, so it's fine to read the actual time. @@ -815,24 +819,21 @@ export function scheduleUpdateOnFiber( } } - ensureRootIsScheduled(root); + ensureRootIsScheduled(root, eventTime); if ( lane === SyncLane && executionContext === NoContext && - (fiber.mode & ConcurrentMode) === NoMode + (fiber.mode & ConcurrentMode) === NoMode && + // Treat `act` as if it's inside `batchedUpdates`, even in legacy mode. + !(__DEV__ && ReactCurrentActQueue.isBatchingLegacy) ) { - if (__DEV__ && ReactCurrentActQueue.isBatchingLegacy) { - // Treat `act` as if it's inside `batchedUpdates`, even in legacy mode. - ReactCurrentActQueue.didScheduleLegacyUpdate = true; - } else { - // Flush the synchronous work now, unless we're already working or inside - // a batch. This is intentionally inside scheduleUpdateOnFiber instead of - // scheduleCallbackForFiber to preserve the ability to schedule a callback - // without immediately flushing it. We only do this for user-initiated - // updates, to preserve historical behavior of legacy mode. - resetRenderTimer(); - flushSyncWorkOnLegacyRootsOnly(); - } + // Flush the synchronous work now, unless we're already working or inside + // a batch. This is intentionally inside scheduleUpdateOnFiber instead of + // scheduleCallbackForFiber to preserve the ability to schedule a callback + // without immediately flushing it. We only do this for user-initiated + // updates, to preserve historical behavior of legacy mode. + resetRenderTimer(); + flushSyncCallbacksOnlyInLegacyMode(); } } } @@ -854,7 +855,7 @@ export function scheduleInitialHydrationOnRoot( const current = root.current; current.lanes = lane; markRootUpdated(root, lane, eventTime); - ensureRootIsScheduled(root); + ensureRootIsScheduled(root, eventTime); } export function isUnsafeClassRenderPhaseUpdate(fiber: Fiber): boolean { @@ -863,12 +864,172 @@ export function isUnsafeClassRenderPhaseUpdate(fiber: Fiber): boolean { return (executionContext & RenderContext) !== NoContext; } +// Use this function to schedule a task for a root. There's only one task per +// root; if a task was already scheduled, we'll check to make sure the priority +// of the existing task is the same as the priority of the next level that the +// root has work on. This function is called on every update, and right before +// exiting a task. +function ensureRootIsScheduled(root: FiberRoot, currentTime: number) { + const existingCallbackNode = root.callbackNode; + + // Check if any lanes are being starved by other work. If so, mark them as + // expired so we know to work on those next. + markStarvedLanesAsExpired(root, currentTime); + + // Determine the next lanes to work on, and their priority. + const nextLanes = getNextLanes( + root, + root === workInProgressRoot ? workInProgressRootRenderLanes : NoLanes, + ); + + if (nextLanes === NoLanes) { + // Special case: There's nothing to work on. + if (existingCallbackNode !== null) { + cancelCallback(existingCallbackNode); + } + root.callbackNode = null; + root.callbackPriority = NoLane; + return; + } + + // If this root is currently suspended and waiting for data to resolve, don't + // schedule a task to render it. We'll either wait for a ping, or wait to + // receive an update. + if ( + workInProgressSuspendedReason === SuspendedOnData && + workInProgressRoot === root + ) { + root.callbackPriority = NoLane; + root.callbackNode = null; + return; + } + + const cancelPendingCommit = root.cancelPendingCommit; + if (cancelPendingCommit !== null) { + // We should only interrupt a pending commit if the new update + // is urgent. + if (includesOnlyNonUrgentLanes(nextLanes)) { + // The new update is not urgent. Don't interrupt the pending commit. + root.callbackPriority = NoLane; + root.callbackNode = null; + return; + } + } + + // We use the highest priority lane to represent the priority of the callback. + const newCallbackPriority = getHighestPriorityLane(nextLanes); + + // Check if there's an existing task. We may be able to reuse it. + const existingCallbackPriority = root.callbackPriority; + if ( + existingCallbackPriority === newCallbackPriority && + // Special case related to `act`. If the currently scheduled task is a + // Scheduler task, rather than an `act` task, cancel it and re-scheduled + // on the `act` queue. + !( + __DEV__ && + ReactCurrentActQueue.current !== null && + existingCallbackNode !== fakeActCallbackNode + ) + ) { + if (__DEV__) { + // If we're going to re-use an existing task, it needs to exist. + // Assume that discrete update microtasks are non-cancellable and null. + // TODO: Temporary until we confirm this warning is not fired. + if ( + existingCallbackNode == null && + !includesSyncLane(existingCallbackPriority) + ) { + console.error( + 'Expected scheduled callback to exist. This error is likely caused by a bug in React. Please file an issue.', + ); + } + } + // The priority hasn't changed. We can reuse the existing task. Exit. + return; + } + + if (existingCallbackNode != null) { + // Cancel the existing callback. We'll schedule a new one below. + cancelCallback(existingCallbackNode); + } + + // Schedule a new callback. + let newCallbackNode; + if (includesSyncLane(newCallbackPriority)) { + // Special case: Sync React callbacks are scheduled on a special + // internal queue + if (root.tag === LegacyRoot) { + if (__DEV__ && ReactCurrentActQueue.isBatchingLegacy !== null) { + ReactCurrentActQueue.didScheduleLegacyUpdate = true; + } + scheduleLegacySyncCallback(performSyncWorkOnRoot.bind(null, root)); + } else { + scheduleSyncCallback(performSyncWorkOnRoot.bind(null, root)); + } + if (supportsMicrotasks) { + // Flush the queue in a microtask. + if (__DEV__ && ReactCurrentActQueue.current !== null) { + // Inside `act`, use our internal `act` queue so that these get flushed + // at the end of the current scope even when using the sync version + // of `act`. + ReactCurrentActQueue.current.push(flushSyncCallbacks); + } else { + scheduleMicrotask(() => { + // In Safari, appending an iframe forces microtasks to run. + // https://github.com/facebook/react/issues/22459 + // We don't support running callbacks in the middle of render + // or commit so we need to check against that. + if ( + (executionContext & (RenderContext | CommitContext)) === + NoContext + ) { + // Note that this would still prematurely flush the callbacks + // if this happens outside render or commit phase (e.g. in an event). + flushSyncCallbacks(); + } + }); + } + } else { + // Flush the queue in an Immediate task. + scheduleCallback(ImmediateSchedulerPriority, flushSyncCallbacks); + } + newCallbackNode = null; + } else { + let schedulerPriorityLevel; + switch (lanesToEventPriority(nextLanes)) { + case DiscreteEventPriority: + schedulerPriorityLevel = ImmediateSchedulerPriority; + break; + case ContinuousEventPriority: + schedulerPriorityLevel = UserBlockingSchedulerPriority; + break; + case DefaultEventPriority: + schedulerPriorityLevel = NormalSchedulerPriority; + break; + case IdleEventPriority: + schedulerPriorityLevel = IdleSchedulerPriority; + break; + default: + schedulerPriorityLevel = NormalSchedulerPriority; + break; + } + newCallbackNode = scheduleCallback( + schedulerPriorityLevel, + performConcurrentWorkOnRoot.bind(null, root), + ); + } + + root.callbackPriority = newCallbackPriority; + root.callbackNode = newCallbackNode; +} + // This is the entry point for every concurrent task, i.e. anything that // goes through Scheduler. -export function performConcurrentWorkOnRoot( +function performConcurrentWorkOnRoot( root: FiberRoot, didTimeout: boolean, -): RenderTaskFn | null { +): $FlowFixMe { if (enableProfilerTimer && enableProfilerNestedUpdatePhase) { resetNestedUpdateFlag(); } @@ -901,7 +1062,6 @@ export function performConcurrentWorkOnRoot( // Determine the next lanes to work on, using the fields stored // on the root. - // TODO: This was already computed in the caller. Pass it as an argument. let lanes = getNextLanes( root, root === workInProgressRoot ? workInProgressRootRenderLanes : NoLanes, @@ -948,7 +1108,7 @@ export function performConcurrentWorkOnRoot( const fatalError = workInProgressRootFatalError; prepareFreshStack(root, NoLanes); markRootSuspended(root, lanes); - ensureRootIsScheduled(root); + ensureRootIsScheduled(root, now()); throw fatalError; } @@ -997,7 +1157,7 @@ export function performConcurrentWorkOnRoot( const fatalError = workInProgressRootFatalError; prepareFreshStack(root, NoLanes); markRootSuspended(root, lanes); - ensureRootIsScheduled(root); + ensureRootIsScheduled(root, now()); throw fatalError; } @@ -1013,8 +1173,13 @@ export function performConcurrentWorkOnRoot( } } - ensureRootIsScheduled(root); - return getContinuationForRoot(root, originalCallbackNode); + ensureRootIsScheduled(root, now()); + if (root.callbackNode === originalCallbackNode) { + // The task node scheduled for this root is the same one that's + // currently executed. Need to return a continuation. + return performConcurrentWorkOnRoot.bind(null, root); + } + return null; } function recoverFromConcurrentError( @@ -1366,7 +1531,7 @@ function markRootSuspended(root: FiberRoot, suspendedLanes: Lanes) { // This is the entry point for synchronous tasks that don't go // through Scheduler -export function performSyncWorkOnRoot(root: FiberRoot): null { +function performSyncWorkOnRoot(root: FiberRoot) { if (enableProfilerTimer && enableProfilerNestedUpdatePhase) { syncNestedUpdateFlag(); } @@ -1377,11 +1542,10 @@ export function performSyncWorkOnRoot(root: FiberRoot): null { flushPassiveEffects(); - // TODO: This was already computed in the caller. Pass it as an argument. let lanes = getNextLanes(root, NoLanes); if (!includesSyncLane(lanes)) { // There's no remaining sync work left. - ensureRootIsScheduled(root); + ensureRootIsScheduled(root, now()); return null; } @@ -1410,7 +1574,7 @@ export function performSyncWorkOnRoot(root: FiberRoot): null { const fatalError = workInProgressRootFatalError; prepareFreshStack(root, NoLanes); markRootSuspended(root, lanes); - ensureRootIsScheduled(root); + ensureRootIsScheduled(root, now()); throw fatalError; } @@ -1419,7 +1583,7 @@ export function performSyncWorkOnRoot(root: FiberRoot): null { // cases where need to exit the current render without producing a // consistent tree or committing. markRootSuspended(root, lanes); - ensureRootIsScheduled(root); + ensureRootIsScheduled(root, now()); return null; } @@ -1436,7 +1600,7 @@ export function performSyncWorkOnRoot(root: FiberRoot): null { // Before exiting, make sure there's a callback scheduled for the next // pending level. - ensureRootIsScheduled(root); + ensureRootIsScheduled(root, now()); return null; } @@ -1444,13 +1608,10 @@ export function performSyncWorkOnRoot(root: FiberRoot): null { export function flushRoot(root: FiberRoot, lanes: Lanes) { if (lanes !== NoLanes) { markRootEntangled(root, mergeLanes(lanes, SyncLane)); - ensureRootIsScheduled(root); + ensureRootIsScheduled(root, now()); if ((executionContext & (RenderContext | CommitContext)) === NoContext) { resetRenderTimer(); - // TODO: For historical reasons this flushes all sync work across all - // roots. It shouldn't really matter either way, but we could change this - // to only flush the given root. - flushSyncWorkOnAllRoots(); + flushSyncCallbacks(); } } } @@ -1488,7 +1649,7 @@ export function batchedUpdates(fn: A => R, a: A): R { !(__DEV__ && ReactCurrentActQueue.isBatchingLegacy) ) { resetRenderTimer(); - flushSyncWorkOnLegacyRootsOnly(); + flushSyncCallbacksOnlyInLegacyMode(); } } } @@ -1556,7 +1717,7 @@ export function flushSync(fn: (() => R) | void): R | void { // Note that this will happen even if batchedUpdates is higher up // the stack. if ((executionContext & (RenderContext | CommitContext)) === NoContext) { - flushSyncWorkOnAllRoots(); + flushSyncCallbacks(); } } } @@ -2152,7 +2313,7 @@ function renderRootConcurrent(root: FiberRoot, lanes: Lanes) { // Ensure the root is scheduled. We should do this even if we're // currently working on a different root, so that we resume // rendering later. - ensureRootIsScheduled(root); + ensureRootIsScheduled(root, now()); }; thenable.then(onResolution, onResolution); break outer; @@ -2989,7 +3150,7 @@ function commitRootImpl( // Always call this before exiting `commitRoot`, to ensure that any // additional work on this root is scheduled. - ensureRootIsScheduled(root); + ensureRootIsScheduled(root, now()); if (recoverableErrors !== null) { // There were errors during this render, but recovered from them without @@ -3044,7 +3205,7 @@ function commitRootImpl( } // If layout work was scheduled, flush it now. - flushSyncWorkOnAllRoots(); + flushSyncCallbacks(); if (__DEV__) { if (enableDebugTracing) { @@ -3250,7 +3411,7 @@ function flushPassiveEffectsImpl() { executionContext = prevExecutionContext; - flushSyncWorkOnAllRoots(); + flushSyncCallbacks(); if (enableTransitionTracing) { const prevPendingTransitionCallbacks = currentPendingTransitionCallbacks; @@ -3335,7 +3496,7 @@ function captureCommitPhaseErrorOnRoot( const eventTime = requestEventTime(); if (root !== null) { markRootUpdated(root, SyncLane, eventTime); - ensureRootIsScheduled(root); + ensureRootIsScheduled(root, eventTime); } } @@ -3378,7 +3539,7 @@ export function captureCommitPhaseError( const eventTime = requestEventTime(); if (root !== null) { markRootUpdated(root, SyncLane, eventTime); - ensureRootIsScheduled(root); + ensureRootIsScheduled(root, eventTime); } return; } @@ -3456,6 +3617,7 @@ function pingSuspendedRoot( pingCache.delete(wakeable); } + const eventTime = requestEventTime(); markRootPinged(root, pingedLanes); warnIfSuspenseResolutionNotWrappedWithActDEV(root); @@ -3498,7 +3660,7 @@ function pingSuspendedRoot( } } - ensureRootIsScheduled(root); + ensureRootIsScheduled(root, eventTime); } function retryTimedOutBoundary(boundaryFiber: Fiber, retryLane: Lane) { @@ -3516,7 +3678,7 @@ function retryTimedOutBoundary(boundaryFiber: Fiber, retryLane: Lane) { const root = enqueueConcurrentRenderForLane(boundaryFiber, retryLane); if (root !== null) { markRootUpdated(root, retryLane, eventTime); - ensureRootIsScheduled(root); + ensureRootIsScheduled(root, eventTime); } } @@ -3998,6 +4160,14 @@ function scheduleCallback(priorityLevel: any, callback) { } } +function cancelCallback(callbackNode: any) { + if (__DEV__ && callbackNode === fakeActCallbackNode) { + return; + } + // In production, always call Scheduler. This function will be stripped out. + return Scheduler_cancelCallback(callbackNode); +} + function shouldForceFlushFallbacksInDEV() { // Never force flush in production. This function should get stripped out. return __DEV__ && ReactCurrentActQueue.current !== null; diff --git a/packages/react-reconciler/src/ReactInternalTypes.js b/packages/react-reconciler/src/ReactInternalTypes.js index ae1e75bfe9467..a672b9669abcf 100644 --- a/packages/react-reconciler/src/ReactInternalTypes.js +++ b/packages/react-reconciler/src/ReactInternalTypes.js @@ -240,10 +240,6 @@ type BaseFiberRootProperties = { MutableSource | MutableSourceVersion, > | null, - // Used to create a linked list that represent all the roots that have - // pending work scheduled on them. - next: FiberRoot | null, - // Node returned by Scheduler.scheduleCallback. Represents the next rendering // task that the root will work on. callbackNode: any, diff --git a/packages/react-reconciler/src/__tests__/ReactFlushSyncNoAggregateError-test.js b/packages/react-reconciler/src/__tests__/ReactFlushSyncNoAggregateError-test.js index 8c1bc7475be36..129b79f743144 100644 --- a/packages/react-reconciler/src/__tests__/ReactFlushSyncNoAggregateError-test.js +++ b/packages/react-reconciler/src/__tests__/ReactFlushSyncNoAggregateError-test.js @@ -3,6 +3,7 @@ let ReactNoop; let Scheduler; let act; let assertLog; +let waitForThrow; let overrideQueueMicrotask; let flushFakeMicrotasks; @@ -42,6 +43,7 @@ describe('ReactFlushSync (AggregateError not available)', () => { const InternalTestUtils = require('internal-test-utils'); assertLog = InternalTestUtils.assertLog; + waitForThrow = InternalTestUtils.waitForThrow; }); function Text({text}) { @@ -93,6 +95,15 @@ describe('ReactFlushSync (AggregateError not available)', () => { // AggregateError is not available, React throws the first error, then // throws the remaining errors in separate tasks. expect(error).toBe(aahh); - expect(flushFakeMicrotasks).toThrow(nooo); + + // TODO: Currently the remaining error is rethrown in an Immediate Scheduler + // task, but this may change to a timer or microtask in the future. The + // exact mechanism is an implementation detail; they just need to be logged + // in the order the occurred. + + // This will start throwing if we change it to rethrow in a microtask. + flushFakeMicrotasks(); + + await waitForThrow(nooo); }); }); diff --git a/packages/react-reconciler/src/__tests__/ReactIncrementalUpdates-test.js b/packages/react-reconciler/src/__tests__/ReactIncrementalUpdates-test.js index b224e4d776482..7413e51a3ab14 100644 --- a/packages/react-reconciler/src/__tests__/ReactIncrementalUpdates-test.js +++ b/packages/react-reconciler/src/__tests__/ReactIncrementalUpdates-test.js @@ -552,8 +552,19 @@ describe('ReactIncrementalUpdates', () => { // The transition should not have expired, so we should be able to // partially render it. await waitFor(['A']); - await waitFor(['B']); - await waitForAll(['C', 'D']); + + // FIXME: We should be able to partially render B, too, but currently it + // expires. This is an existing bug that I discovered, which will be fixed + // in a PR that I'm currently working on. + // + // Correct behavior: + // await waitFor(['B']); + // await waitForAll(['C', 'D']); + // + // Current behavior: + await waitFor(['B'], { + additionalLogsAfterAttemptingToYield: ['C', 'D'], + }); }); it('regression: does not expire soon due to previous expired work', async () => { diff --git a/packages/react/src/__tests__/ReactProfiler-test.internal.js b/packages/react/src/__tests__/ReactProfiler-test.internal.js index de6b7f973e66b..3602a0190be65 100644 --- a/packages/react/src/__tests__/ReactProfiler-test.internal.js +++ b/packages/react/src/__tests__/ReactProfiler-test.internal.js @@ -258,22 +258,13 @@ describe(`onRender`, () => { // TODO: unstable_now is called by more places than just the profiler. // Rewrite this test so it's less fragile. - if (gate(flags => flags.enableDeferRootSchedulingToMicrotask)) { - assertLog([ - 'read current time', - 'read current time', - 'read current time', - ]); - } else { - assertLog([ - 'read current time', - 'read current time', - 'read current time', - 'read current time', - 'read current time', - 'read current time', - ]); - } + assertLog([ + 'read current time', + 'read current time', + 'read current time', + 'read current time', + 'read current time', + ]); // Restore original mock jest.mock('scheduler', () => jest.requireActual('scheduler/unstable_mock')); diff --git a/packages/shared/ReactFeatureFlags.js b/packages/shared/ReactFeatureFlags.js index 619b4bf152a9b..6d20fe0f9a9a2 100644 --- a/packages/shared/ReactFeatureFlags.js +++ b/packages/shared/ReactFeatureFlags.js @@ -48,10 +48,6 @@ export const enableSchedulerDebugging = false; // Need to remove didTimeout argument from Scheduler before landing export const disableSchedulerTimeoutInWorkLoop = false; -// This will break some internal tests at Meta so we need to gate this until -// those can be fixed. -export const enableDeferRootSchedulingToMicrotask = true; - // ----------------------------------------------------------------------------- // Slated for removal in the future (significant effort) // diff --git a/packages/shared/forks/ReactFeatureFlags.native-fb-dynamic.js b/packages/shared/forks/ReactFeatureFlags.native-fb-dynamic.js index 2270b18ae3340..b7bfa74d1590f 100644 --- a/packages/shared/forks/ReactFeatureFlags.native-fb-dynamic.js +++ b/packages/shared/forks/ReactFeatureFlags.native-fb-dynamic.js @@ -21,7 +21,6 @@ import typeof * as DynamicFlagsType from 'ReactNativeInternalFeatureFlags'; // update the test configuration. export const enableUseRefAccessWarning = __VARIANT__; -export const enableDeferRootSchedulingToMicrotask = __VARIANT__; // Flow magic to verify the exports of this file match the original version. ((((null: any): ExportsType): DynamicFlagsType): ExportsType); diff --git a/packages/shared/forks/ReactFeatureFlags.native-fb.js b/packages/shared/forks/ReactFeatureFlags.native-fb.js index 3b54b4d9d60d1..726a8aedb7916 100644 --- a/packages/shared/forks/ReactFeatureFlags.native-fb.js +++ b/packages/shared/forks/ReactFeatureFlags.native-fb.js @@ -17,8 +17,7 @@ import * as dynamicFlags from 'ReactNativeInternalFeatureFlags'; // We destructure each value before re-exporting to avoid a dynamic look-up on // the exports object every time a flag is read. -export const {enableUseRefAccessWarning, enableDeferRootSchedulingToMicrotask} = - dynamicFlags; +export const {enableUseRefAccessWarning} = dynamicFlags; // The rest of the flags are static for better dead code elimination. export const enableDebugTracing = false; diff --git a/packages/shared/forks/ReactFeatureFlags.native-oss.js b/packages/shared/forks/ReactFeatureFlags.native-oss.js index 0efdd6e8b18c1..b069cb3bf7fb8 100644 --- a/packages/shared/forks/ReactFeatureFlags.native-oss.js +++ b/packages/shared/forks/ReactFeatureFlags.native-oss.js @@ -69,7 +69,6 @@ export const enableHostSingletons = true; export const useModernStrictMode = false; export const enableFizzExternalRuntime = false; -export const enableDeferRootSchedulingToMicrotask = true; // Flow magic to verify the exports of this file match the original version. ((((null: any): ExportsType): FeatureFlagsType): ExportsType); diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.js index f4d7962376d46..d71e48ee9282d 100644 --- a/packages/shared/forks/ReactFeatureFlags.test-renderer.js +++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.js @@ -69,7 +69,6 @@ export const enableHostSingletons = true; export const useModernStrictMode = false; export const enableFizzExternalRuntime = false; -export const enableDeferRootSchedulingToMicrotask = true; // Flow magic to verify the exports of this file match the original version. ((((null: any): ExportsType): FeatureFlagsType): ExportsType); diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.native.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.native.js index 4dbf373f8c182..392a73ecd7b40 100644 --- a/packages/shared/forks/ReactFeatureFlags.test-renderer.native.js +++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.native.js @@ -66,7 +66,6 @@ export const enableFloat = true; export const enableHostSingletons = true; export const useModernStrictMode = false; -export const enableDeferRootSchedulingToMicrotask = true; // Flow magic to verify the exports of this file match the original version. ((((null: any): ExportsType): FeatureFlagsType): ExportsType); diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js index a193740fb0ab7..54eb8bd7bc45c 100644 --- a/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js +++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js @@ -71,7 +71,6 @@ export const enableHostSingletons = true; export const useModernStrictMode = false; export const enableFizzExternalRuntime = false; -export const enableDeferRootSchedulingToMicrotask = true; // Flow magic to verify the exports of this file match the original version. ((((null: any): ExportsType): FeatureFlagsType): ExportsType); diff --git a/packages/shared/forks/ReactFeatureFlags.www-dynamic.js b/packages/shared/forks/ReactFeatureFlags.www-dynamic.js index 69c5fad8938a6..64670368b8401 100644 --- a/packages/shared/forks/ReactFeatureFlags.www-dynamic.js +++ b/packages/shared/forks/ReactFeatureFlags.www-dynamic.js @@ -23,7 +23,6 @@ export const enableLazyContextPropagation = __VARIANT__; export const enableUnifiedSyncLane = __VARIANT__; export const enableTransitionTracing = __VARIANT__; export const enableCustomElementPropertySupport = __VARIANT__; -export const enableDeferRootSchedulingToMicrotask = __VARIANT__; // Enable this flag to help with concurrent mode debugging. // It logs information to the console about React scheduling, rendering, and commit phases. diff --git a/packages/shared/forks/ReactFeatureFlags.www.js b/packages/shared/forks/ReactFeatureFlags.www.js index 229b60c37f155..00f9ad5d00bc0 100644 --- a/packages/shared/forks/ReactFeatureFlags.www.js +++ b/packages/shared/forks/ReactFeatureFlags.www.js @@ -27,7 +27,6 @@ export const { enableUnifiedSyncLane, enableTransitionTracing, enableCustomElementPropertySupport, - enableDeferRootSchedulingToMicrotask, } = dynamicFeatureFlags; // On WWW, __EXPERIMENTAL__ is used for a new modern build. diff --git a/scripts/flow/xplat.js b/scripts/flow/xplat.js index 8e14cf5a04185..5a0d26b66f667 100644 --- a/scripts/flow/xplat.js +++ b/scripts/flow/xplat.js @@ -9,5 +9,4 @@ declare module 'ReactNativeInternalFeatureFlags' { declare export var enableUseRefAccessWarning: boolean; - declare export var enableDeferRootSchedulingToMicrotask: boolean; }