diff --git a/packages/react-dom/index.classic.fb.js b/packages/react-dom/index.classic.fb.js index d6bc031477bff..ea9dd9aba257e 100644 --- a/packages/react-dom/index.classic.fb.js +++ b/packages/react-dom/index.classic.fb.js @@ -34,6 +34,7 @@ export { createBlockingRoot as unstable_createBlockingRoot, unstable_flushControlled, unstable_scheduleHydration, + unstable_runWithPriority, unstable_renderSubtreeIntoContainer, unstable_createPortal, unstable_createEventHandle, diff --git a/packages/react-dom/index.experimental.js b/packages/react-dom/index.experimental.js index ba1d58b72ae21..9ed70f3959ec9 100644 --- a/packages/react-dom/index.experimental.js +++ b/packages/react-dom/index.experimental.js @@ -23,6 +23,8 @@ export { createBlockingRoot as unstable_createBlockingRoot, unstable_flushControlled, unstable_scheduleHydration, + // DO NOT USE: Temporarily exposing this to migrate off of Scheduler.runWithPriority. + unstable_runWithPriority, // Disabled behind disableUnstableRenderSubtreeIntoContainer unstable_renderSubtreeIntoContainer, // Disabled behind disableUnstableCreatePortal diff --git a/packages/react-dom/index.js b/packages/react-dom/index.js index a3bc225bb8c86..59825272c3a89 100644 --- a/packages/react-dom/index.js +++ b/packages/react-dom/index.js @@ -25,6 +25,7 @@ export { createBlockingRoot as unstable_createBlockingRoot, unstable_flushControlled, unstable_scheduleHydration, + unstable_runWithPriority, unstable_renderSubtreeIntoContainer, unstable_createPortal, unstable_createEventHandle, diff --git a/packages/react-dom/index.modern.fb.js b/packages/react-dom/index.modern.fb.js index c9317869ee1ba..addcce97749c9 100644 --- a/packages/react-dom/index.modern.fb.js +++ b/packages/react-dom/index.modern.fb.js @@ -19,6 +19,7 @@ export { createBlockingRoot as unstable_createBlockingRoot, unstable_flushControlled, unstable_scheduleHydration, + unstable_runWithPriority, unstable_createEventHandle, unstable_isNewReconciler, } from './src/client/ReactDOM'; diff --git a/packages/react-dom/src/client/ReactDOM.js b/packages/react-dom/src/client/ReactDOM.js index 1887c6ffd8360..078d175f8673d 100644 --- a/packages/react-dom/src/client/ReactDOM.js +++ b/packages/react-dom/src/client/ReactDOM.js @@ -35,6 +35,8 @@ import { attemptUserBlockingHydration, attemptContinuousHydration, attemptHydrationAtCurrentPriority, + runWithPriority, + getCurrentUpdatePriority, } from 'react-reconciler/src/ReactFiberReconciler'; import {createPortal as createPortalImpl} from 'react-reconciler/src/ReactPortal'; import {canUseDOM} from 'shared/ExecutionEnvironment'; @@ -58,6 +60,8 @@ import { setAttemptContinuousHydration, setAttemptHydrationAtCurrentPriority, queueExplicitHydrationTarget, + setGetCurrentUpdatePriority, + setAttemptHydrationAtPriority, } from '../events/ReactDOMEventReplaying'; import {setBatchingImplementation} from '../events/ReactDOMUpdateBatching'; import { @@ -70,6 +74,8 @@ setAttemptSynchronousHydration(attemptSynchronousHydration); setAttemptUserBlockingHydration(attemptUserBlockingHydration); setAttemptContinuousHydration(attemptContinuousHydration); setAttemptHydrationAtCurrentPriority(attemptHydrationAtCurrentPriority); +setGetCurrentUpdatePriority(getCurrentUpdatePriority); +setAttemptHydrationAtPriority(runWithPriority); let didWarnAboutUnstableCreatePortal = false; let didWarnAboutUnstableRenderSubtreeIntoContainer = false; @@ -205,6 +211,9 @@ export { unstable_createPortal, // enableCreateEventHandleAPI createEventHandle as unstable_createEventHandle, + // TODO: Remove this once callers migrate to alternatives. + // This should only be used by React internals. + runWithPriority as unstable_runWithPriority, }; const foundDevTools = injectIntoDevTools({ diff --git a/packages/react-dom/src/events/ReactDOMEventReplaying.js b/packages/react-dom/src/events/ReactDOMEventReplaying.js index d14c7587f257a..9251da4eaa637 100644 --- a/packages/react-dom/src/events/ReactDOMEventReplaying.js +++ b/packages/react-dom/src/events/ReactDOMEventReplaying.js @@ -12,7 +12,10 @@ import type {Container, SuspenseInstance} from '../client/ReactDOMHostConfig'; import type {DOMTopLevelEventType} from '../events/TopLevelEventTypes'; import type {ElementListenerMap} from '../client/ReactDOMComponentTree'; import type {EventSystemFlags} from './EventSystemFlags'; -import type {FiberRoot} from 'react-reconciler/src/ReactInternalTypes'; +import type { + FiberRoot, + ReactPriorityLevel, +} from 'react-reconciler/src/ReactInternalTypes'; import { enableDeprecatedFlareAPI, @@ -64,6 +67,23 @@ export function setAttemptHydrationAtCurrentPriority( attemptHydrationAtCurrentPriority = fn; } +let getCurrentUpdatePriority: () => ReactPriorityLevel; + +export function setGetCurrentUpdatePriority(fn: () => ReactPriorityLevel) { + getCurrentUpdatePriority = fn; +} + +let attemptHydrationAtPriority: ( + priority: ReactPriorityLevel, + fn: () => T, +) => T; + +export function setAttemptHydrationAtPriority( + fn: (priority: ReactPriorityLevel, fn: () => T) => T, +) { + attemptHydrationAtPriority = fn; +} + // TODO: Upgrade this definition once we're on a newer version of Flow that // has this definition built-in. type PointerEvent = Event & { @@ -147,6 +167,7 @@ type QueuedHydrationTarget = {| blockedOn: null | Container | SuspenseInstance, target: Node, priority: number, + lanePriority: ReactPriorityLevel, |}; const queuedExplicitHydrationTargets: Array = []; @@ -508,9 +529,12 @@ function attemptExplicitHydrationTarget( // We're blocked on hydrating this boundary. // Increase its priority. queuedTarget.blockedOn = instance; - runWithPriority(queuedTarget.priority, () => { - attemptHydrationAtCurrentPriority(nearestMounted); + attemptHydrationAtPriority(queuedTarget.lanePriority, () => { + runWithPriority(queuedTarget.priority, () => { + attemptHydrationAtCurrentPriority(nearestMounted); + }); }); + return; } } else if (tag === HostRoot) { @@ -529,15 +553,17 @@ function attemptExplicitHydrationTarget( export function queueExplicitHydrationTarget(target: Node): void { if (enableSelectiveHydration) { - const priority = getCurrentPriorityLevel(); + const schedulerPriority = getCurrentPriorityLevel(); + const updateLanePriority = getCurrentUpdatePriority(); const queuedTarget: QueuedHydrationTarget = { blockedOn: null, target: target, - priority: priority, + priority: schedulerPriority, + lanePriority: updateLanePriority, }; let i = 0; for (; i < queuedExplicitHydrationTargets.length; i++) { - if (priority <= queuedExplicitHydrationTargets[i].priority) { + if (schedulerPriority <= queuedExplicitHydrationTargets[i].priority) { break; } } diff --git a/packages/react-dom/src/events/plugins/__tests__/ModernSimpleEventPlugin-test.js b/packages/react-dom/src/events/plugins/__tests__/ModernSimpleEventPlugin-test.js index a65019f4df12b..0063fa417e7f4 100644 --- a/packages/react-dom/src/events/plugins/__tests__/ModernSimpleEventPlugin-test.js +++ b/packages/react-dom/src/events/plugins/__tests__/ModernSimpleEventPlugin-test.js @@ -231,6 +231,8 @@ describe('SimpleEventPlugin', function() { describe('interactive events, in concurrent mode', () => { beforeEach(() => { jest.resetModules(); + + React = require('react'); ReactDOM = require('react-dom'); Scheduler = require('scheduler'); }); @@ -377,11 +379,14 @@ describe('SimpleEventPlugin', function() { diff --git a/packages/react-noop-renderer/src/ReactNoop.js b/packages/react-noop-renderer/src/ReactNoop.js index e463b77aa9b1a..6695256834226 100644 --- a/packages/react-noop-renderer/src/ReactNoop.js +++ b/packages/react-noop-renderer/src/ReactNoop.js @@ -47,6 +47,9 @@ export const { act, dumpTree, getRoot, + // TODO: Remove this once callers migrate to alternatives. + // This should only be used by React internals. + unstable_runWithPriority, } = createReactNoop( ReactFiberReconciler, // reconciler true, // useMutation diff --git a/packages/react-noop-renderer/src/ReactNoopPersistent.js b/packages/react-noop-renderer/src/ReactNoopPersistent.js index dd9aa34d7d106..845c8d3acc1a3 100644 --- a/packages/react-noop-renderer/src/ReactNoopPersistent.js +++ b/packages/react-noop-renderer/src/ReactNoopPersistent.js @@ -47,6 +47,9 @@ export const { act, dumpTree, getRoot, + // TODO: Remove this once callers migrate to alternatives. + // This should only be used by React internals. + unstable_runWithPriority, } = createReactNoop( ReactFiberReconciler, // reconciler false, // useMutation diff --git a/packages/react-noop-renderer/src/createReactNoop.js b/packages/react-noop-renderer/src/createReactNoop.js index 5c5e47bec89fb..d6ed76cfcc57f 100644 --- a/packages/react-noop-renderer/src/createReactNoop.js +++ b/packages/react-noop-renderer/src/createReactNoop.js @@ -954,6 +954,8 @@ function createReactNoop(reconciler: Function, useMutation: boolean) { return Scheduler.unstable_flushExpired(); }, + unstable_runWithPriority: NoopRenderer.runWithPriority, + batchedUpdates: NoopRenderer.batchedUpdates, deferredUpdates: NoopRenderer.deferredUpdates, diff --git a/packages/react-reconciler/src/ReactFiberHooks.new.js b/packages/react-reconciler/src/ReactFiberHooks.new.js index f21e8273ad824..90afafc7a7abd 100644 --- a/packages/react-reconciler/src/ReactFiberHooks.new.js +++ b/packages/react-reconciler/src/ReactFiberHooks.new.js @@ -30,11 +30,16 @@ import {NoMode, BlockingMode} from './ReactTypeOfMode'; import { NoLane, NoLanes, + InputContinuousLanePriority, isSubsetOfLanes, mergeLanes, removeLanes, markRootEntangled, markRootMutableRead, + getCurrentUpdateLanePriority, + setCurrentUpdateLanePriority, + higherLanePriority, + DefaultLanePriority, } from './ReactFiberLane'; import {readContext} from './ReactFiberNewContext.new'; import {createDeprecatedResponderListener} from './ReactFiberDeprecatedEvents.new'; @@ -1498,12 +1503,20 @@ function rerenderDeferredValue( function startTransition(setPending, config, callback) { const priorityLevel = getCurrentPriorityLevel(); + const previousLanePriority = getCurrentUpdateLanePriority(); + setCurrentUpdateLanePriority( + higherLanePriority(previousLanePriority, InputContinuousLanePriority), + ); runWithPriority( priorityLevel < UserBlockingPriority ? UserBlockingPriority : priorityLevel, () => { setPending(true); }, ); + + // If there's no SuspenseConfig set, we'll use the DefaultLanePriority for this transition. + setCurrentUpdateLanePriority(DefaultLanePriority); + runWithPriority( priorityLevel > NormalPriority ? NormalPriority : priorityLevel, () => { @@ -1513,6 +1526,7 @@ function startTransition(setPending, config, callback) { setPending(false); callback(); } finally { + setCurrentUpdateLanePriority(previousLanePriority); ReactCurrentBatchConfig.suspense = previousConfig; } }, diff --git a/packages/react-reconciler/src/ReactFiberHooks.old.js b/packages/react-reconciler/src/ReactFiberHooks.old.js index 44522a725966d..95f2e4f5c0c6a 100644 --- a/packages/react-reconciler/src/ReactFiberHooks.old.js +++ b/packages/react-reconciler/src/ReactFiberHooks.old.js @@ -33,11 +33,16 @@ import {NoMode, BlockingMode, DebugTracingMode} from './ReactTypeOfMode'; import { NoLane, NoLanes, + InputContinuousLanePriority, isSubsetOfLanes, mergeLanes, removeLanes, markRootEntangled, markRootMutableRead, + getCurrentUpdateLanePriority, + setCurrentUpdateLanePriority, + higherLanePriority, + DefaultLanePriority, } from './ReactFiberLane'; import {readContext} from './ReactFiberNewContext.old'; import {createDeprecatedResponderListener} from './ReactFiberDeprecatedEvents.old'; @@ -1502,12 +1507,20 @@ function rerenderDeferredValue( function startTransition(setPending, config, callback) { const priorityLevel = getCurrentPriorityLevel(); + const previousLanePriority = getCurrentUpdateLanePriority(); + setCurrentUpdateLanePriority( + higherLanePriority(previousLanePriority, InputContinuousLanePriority), + ); runWithPriority( priorityLevel < UserBlockingPriority ? UserBlockingPriority : priorityLevel, () => { setPending(true); }, ); + + // If there's no SuspenseConfig set, we'll use the DefaultLanePriority for this transition. + setCurrentUpdateLanePriority(DefaultLanePriority); + runWithPriority( priorityLevel > NormalPriority ? NormalPriority : priorityLevel, () => { @@ -1517,6 +1530,7 @@ function startTransition(setPending, config, callback) { setPending(false); callback(); } finally { + setCurrentUpdateLanePriority(previousLanePriority); ReactCurrentBatchConfig.suspense = previousConfig; } }, diff --git a/packages/react-reconciler/src/ReactFiberLane.js b/packages/react-reconciler/src/ReactFiberLane.js index eae1194836d9c..843582cd9d177 100644 --- a/packages/react-reconciler/src/ReactFiberLane.js +++ b/packages/react-reconciler/src/ReactFiberLane.js @@ -49,10 +49,10 @@ const InputDiscreteHydrationLanePriority: LanePriority = 14; export const InputDiscreteLanePriority: LanePriority = 13; const InputContinuousHydrationLanePriority: LanePriority = 12; -const InputContinuousLanePriority: LanePriority = 11; +export const InputContinuousLanePriority: LanePriority = 11; const DefaultHydrationLanePriority: LanePriority = 10; -const DefaultLanePriority: LanePriority = 9; +export const DefaultLanePriority: LanePriority = 9; const TransitionShortHydrationLanePriority: LanePriority = 8; export const TransitionShortLanePriority: LanePriority = 7; @@ -120,6 +120,16 @@ export const OffscreenLane: Lane = /* */ 0b1000000000000000000 export const NoTimestamp = -1; +let currentUpdateLanePriority: LanePriority = NoLanePriority; + +export function getCurrentUpdateLanePriority(): LanePriority { + return currentUpdateLanePriority; +} + +export function setCurrentUpdateLanePriority(newLanePriority: LanePriority) { + currentUpdateLanePriority = newLanePriority; +} + // "Registers" used to "return" multiple values // Used by getHighestPriorityLanes and getNextLanes: let return_highestLanePriority: LanePriority = DefaultLanePriority; @@ -651,6 +661,13 @@ export function higherPriorityLane(a: Lane, b: Lane) { return a !== NoLane && a < b ? a : b; } +export function higherLanePriority( + a: LanePriority, + b: LanePriority, +): LanePriority { + return a !== NoLanePriority && a > b ? a : b; +} + export function createLaneMap(initial: T): LaneMap { return new Array(TotalLanes).fill(initial); } diff --git a/packages/react-reconciler/src/ReactFiberReconciler.js b/packages/react-reconciler/src/ReactFiberReconciler.js index 434ca784e759c..d025fc2098160 100644 --- a/packages/react-reconciler/src/ReactFiberReconciler.js +++ b/packages/react-reconciler/src/ReactFiberReconciler.js @@ -50,6 +50,8 @@ import { focusWithin as focusWithin_old, observeVisibleRects as observeVisibleRects_old, registerMutableSourceForHydration as registerMutableSourceForHydration_old, + runWithPriority as runWithPriority_old, + getCurrentUpdatePriority as getCurrentUpdatePriority_old, } from './ReactFiberReconciler.old'; import { @@ -88,6 +90,8 @@ import { focusWithin as focusWithin_new, observeVisibleRects as observeVisibleRects_new, registerMutableSourceForHydration as registerMutableSourceForHydration_new, + runWithPriority as runWithPriority_new, + getCurrentUpdatePriority as getCurrentUpdatePriority_new, } from './ReactFiberReconciler.new'; export const createContainer = enableNewReconciler @@ -139,6 +143,9 @@ export const attemptContinuousHydration = enableNewReconciler export const attemptHydrationAtCurrentPriority = enableNewReconciler ? attemptHydrationAtCurrentPriority_new : attemptHydrationAtCurrentPriority_old; +export const getCurrentUpdatePriority = enableNewReconciler + ? getCurrentUpdatePriority_new + : getCurrentUpdatePriority_old; export const findHostInstance = enableNewReconciler ? findHostInstance_new : findHostInstance_old; @@ -194,3 +201,6 @@ export const observeVisibleRects = enableNewReconciler export const registerMutableSourceForHydration = enableNewReconciler ? registerMutableSourceForHydration_new : registerMutableSourceForHydration_old; +export const runWithPriority = enableNewReconciler + ? runWithPriority_new + : runWithPriority_old; diff --git a/packages/react-reconciler/src/ReactFiberReconciler.new.js b/packages/react-reconciler/src/ReactFiberReconciler.new.js index 3d508dec78aea..6b1b1c86768b0 100644 --- a/packages/react-reconciler/src/ReactFiberReconciler.new.js +++ b/packages/react-reconciler/src/ReactFiberReconciler.new.js @@ -7,7 +7,11 @@ * @flow */ -import type {Fiber, SuspenseHydrationCallbacks} from './ReactInternalTypes'; +import type { + Fiber, + ReactPriorityLevel, + SuspenseHydrationCallbacks, +} from './ReactInternalTypes'; import type {FiberRoot} from './ReactInternalTypes'; import type {RootTag} from './ReactRootTags'; import type { @@ -79,6 +83,10 @@ import { NoTimestamp, getHighestPriorityPendingLanes, higherPriorityLane, + getCurrentUpdateLanePriority, + setCurrentUpdateLanePriority, + schedulerPriorityToLanePriority, + lanePriorityToSchedulerPriority, } from './ReactFiberLane'; import {requestCurrentSuspenseConfig} from './ReactFiberSuspenseConfig'; import { @@ -424,6 +432,20 @@ export function attemptHydrationAtCurrentPriority(fiber: Fiber): void { markRetryLaneIfNotHydrated(fiber, lane); } +export function runWithPriority(priority: ReactPriorityLevel, fn: () => T) { + const previousPriority = getCurrentUpdateLanePriority(); + try { + setCurrentUpdateLanePriority(schedulerPriorityToLanePriority(priority)); + return fn(); + } finally { + setCurrentUpdateLanePriority(previousPriority); + } +} + +export function getCurrentUpdatePriority(): ReactPriorityLevel { + return lanePriorityToSchedulerPriority(getCurrentUpdateLanePriority()); +} + export {findHostInstance}; export {findHostInstanceWithWarning}; diff --git a/packages/react-reconciler/src/ReactFiberReconciler.old.js b/packages/react-reconciler/src/ReactFiberReconciler.old.js index 79b44909d93d5..da1918bcd2a2b 100644 --- a/packages/react-reconciler/src/ReactFiberReconciler.old.js +++ b/packages/react-reconciler/src/ReactFiberReconciler.old.js @@ -7,7 +7,11 @@ * @flow */ -import type {Fiber, SuspenseHydrationCallbacks} from './ReactInternalTypes'; +import type { + Fiber, + ReactPriorityLevel, + SuspenseHydrationCallbacks, +} from './ReactInternalTypes'; import type {FiberRoot} from './ReactInternalTypes'; import type {RootTag} from './ReactRootTags'; import type { @@ -79,6 +83,10 @@ import { NoTimestamp, getHighestPriorityPendingLanes, higherPriorityLane, + getCurrentUpdateLanePriority, + setCurrentUpdateLanePriority, + schedulerPriorityToLanePriority, + lanePriorityToSchedulerPriority, } from './ReactFiberLane'; import {requestCurrentSuspenseConfig} from './ReactFiberSuspenseConfig'; import { @@ -424,6 +432,20 @@ export function attemptHydrationAtCurrentPriority(fiber: Fiber): void { markRetryLaneIfNotHydrated(fiber, lane); } +export function runWithPriority(priority: ReactPriorityLevel, fn: () => T) { + const previousPriority = getCurrentUpdateLanePriority(); + try { + setCurrentUpdateLanePriority(schedulerPriorityToLanePriority(priority)); + return fn(); + } finally { + setCurrentUpdateLanePriority(previousPriority); + } +} + +export function getCurrentUpdatePriority(): ReactPriorityLevel { + return lanePriorityToSchedulerPriority(getCurrentUpdateLanePriority()); +} + export {findHostInstance}; export {findHostInstanceWithWarning}; diff --git a/packages/react-reconciler/src/ReactFiberWorkLoop.new.js b/packages/react-reconciler/src/ReactFiberWorkLoop.new.js index d3ee13fda5fd2..9ca87567bcd9c 100644 --- a/packages/react-reconciler/src/ReactFiberWorkLoop.new.js +++ b/packages/react-reconciler/src/ReactFiberWorkLoop.new.js @@ -26,6 +26,7 @@ import { enableSchedulerTracing, warnAboutUnmockedScheduler, deferRenderPhaseUpdateToNextBatch, + decoupleUpdatePriorityFromScheduler, } from 'shared/ReactFeatureFlags'; import ReactSharedInternals from 'shared/ReactSharedInternals'; import invariant from 'shared/invariant'; @@ -113,6 +114,7 @@ import { InputDiscreteLanePriority, TransitionShortLanePriority, TransitionLongLanePriority, + DefaultLanePriority, NoLanes, NoLane, SyncLane, @@ -130,6 +132,8 @@ import { hasUpdatePriority, getNextLanes, returnNextLanesPriority, + setCurrentUpdateLanePriority, + getCurrentUpdateLanePriority, markStarvedLanesAsExpired, getLanesToRetrySynchronouslyOnError, markRootUpdated, @@ -394,7 +398,6 @@ export function requestUpdateLane( currentEventWipLanes = workInProgressRootIncludedLanes; } - let lane; if (suspenseConfig !== null) { // Use the size of the timeout as a heuristic to prioritize shorter // transitions over longer ones. @@ -412,26 +415,56 @@ export function requestUpdateLane( : NoLanes; } - lane = findTransitionLane( + return findTransitionLane( transitionLanePriority, currentEventWipLanes, currentEventPendingLanes, ); + } + + // TODO: Remove this dependency on the Scheduler priority. + // To do that, we're replacing it with an update lane priority. + const schedulerPriority = getCurrentPriorityLevel(); + + // The old behavior was using the priority level of the Scheduler. + // This couples React to the Scheduler internals, so we're replacing it + // with the currentUpdateLanePriority above. As an example of how this + // could be problematic, if we're not inside `Scheduler.runWithPriority`, + // then we'll get the priority of the current running Scheduler task, + // which is probably not what we want. + let lane; + if ( + // TODO: Temporary. We're removing the concept of discrete updates. + (executionContext & DiscreteEventContext) !== NoContext && + schedulerPriority === UserBlockingSchedulerPriority + ) { + lane = findUpdateLane(InputDiscreteLanePriority, currentEventWipLanes); } else { - // TODO: If we're not inside `runWithPriority`, this returns the priority - // of the currently running task. That's probably not what we want. - const schedulerPriority = getCurrentPriorityLevel(); + const schedulerLanePriority = schedulerPriorityToLanePriority( + schedulerPriority, + ); - if ( - // TODO: Temporary. We're removing the concept of discrete updates. - (executionContext & DiscreteEventContext) !== NoContext && - schedulerPriority === UserBlockingSchedulerPriority - ) { - lane = findUpdateLane(InputDiscreteLanePriority, currentEventWipLanes); - } else { - const lanePriority = schedulerPriorityToLanePriority(schedulerPriority); - lane = findUpdateLane(lanePriority, currentEventWipLanes); + if (decoupleUpdatePriorityFromScheduler) { + // In the new strategy, we will track the current update lane priority + // inside React and use that priority to select a lane for this update. + // For now, we're just logging when they're different so we can assess. + const currentUpdateLanePriority = getCurrentUpdateLanePriority(); + + if ( + schedulerLanePriority !== currentUpdateLanePriority && + currentUpdateLanePriority !== NoLanePriority + ) { + if (__DEV__) { + console.error( + 'Expected current scheduler lane priority %s to match current update lane priority %s', + schedulerLanePriority, + currentUpdateLanePriority, + ); + } + } } + + lane = findUpdateLane(schedulerLanePriority, currentEventWipLanes); } return lane; @@ -1068,7 +1101,13 @@ export function flushDiscreteUpdates() { export function deferredUpdates(fn: () => A): A { // TODO: Remove in favor of Scheduler.next - return runWithPriority(NormalSchedulerPriority, fn); + const previousLanePriority = getCurrentUpdateLanePriority(); + try { + setCurrentUpdateLanePriority(DefaultLanePriority); + return runWithPriority(NormalSchedulerPriority, fn); + } finally { + setCurrentUpdateLanePriority(previousLanePriority); + } } function flushPendingDiscreteUpdates() { @@ -1123,13 +1162,16 @@ export function discreteUpdates( ): R { const prevExecutionContext = executionContext; executionContext |= DiscreteEventContext; + const previousLanePriority = getCurrentUpdateLanePriority(); try { + setCurrentUpdateLanePriority(InputDiscreteLanePriority); // Should this return runWithPriority( UserBlockingSchedulerPriority, fn.bind(null, a, b, c, d), ); } finally { + setCurrentUpdateLanePriority(previousLanePriority); executionContext = prevExecutionContext; if (executionContext === NoContext) { // Flush the immediate callbacks that were scheduled during this batch @@ -1166,13 +1208,16 @@ export function flushSync(fn: A => R, a: A): R { return fn(a); } executionContext |= BatchedContext; + const previousLanePriority = getCurrentUpdateLanePriority(); try { + setCurrentUpdateLanePriority(SyncLanePriority); if (fn) { return runWithPriority(ImmediateSchedulerPriority, fn.bind(null, a)); } else { return (undefined: $FlowFixMe); } } finally { + setCurrentUpdateLanePriority(previousLanePriority); executionContext = prevExecutionContext; // Flush the immediate callbacks that were scheduled during this batch. // Note that this will happen even if batchedUpdates is higher up @@ -1184,9 +1229,12 @@ export function flushSync(fn: A => R, a: A): R { export function flushControlled(fn: () => mixed): void { const prevExecutionContext = executionContext; executionContext |= BatchedContext; + const previousLanePriority = getCurrentUpdateLanePriority(); try { + setCurrentUpdateLanePriority(SyncLanePriority); runWithPriority(ImmediateSchedulerPriority, fn); } finally { + setCurrentUpdateLanePriority(previousLanePriority); executionContext = prevExecutionContext; if (executionContext === NoContext) { // Flush the immediate callbacks that were scheduled during this batch @@ -1867,6 +1915,9 @@ function commitRootImpl(root, renderPriorityLevel) { } if (firstEffect !== null) { + const previousLanePriority = getCurrentUpdateLanePriority(); + setCurrentUpdateLanePriority(SyncLanePriority); + const prevExecutionContext = executionContext; executionContext |= CommitContext; const prevInteractions = pushInteractions(root); @@ -1987,6 +2038,9 @@ function commitRootImpl(root, renderPriorityLevel) { popInteractions(((prevInteractions: any): Set)); } executionContext = prevExecutionContext; + + // Reset the priority to the previous non-sync value. + setCurrentUpdateLanePriority(previousLanePriority); } else { // No effects. root.current = finishedWork; @@ -2249,7 +2303,15 @@ export function flushPassiveEffects() { ? NormalSchedulerPriority : pendingPassiveEffectsRenderPriority; pendingPassiveEffectsRenderPriority = NoSchedulerPriority; - return runWithPriority(priorityLevel, flushPassiveEffectsImpl); + const previousLanePriority = getCurrentUpdateLanePriority(); + try { + setCurrentUpdateLanePriority( + schedulerPriorityToLanePriority(priorityLevel), + ); + return runWithPriority(priorityLevel, flushPassiveEffectsImpl); + } finally { + setCurrentUpdateLanePriority(previousLanePriority); + } } } diff --git a/packages/react-reconciler/src/ReactFiberWorkLoop.old.js b/packages/react-reconciler/src/ReactFiberWorkLoop.old.js index 4d5469b04ed44..789da343aaefb 100644 --- a/packages/react-reconciler/src/ReactFiberWorkLoop.old.js +++ b/packages/react-reconciler/src/ReactFiberWorkLoop.old.js @@ -26,6 +26,7 @@ import { enableSchedulerTracing, warnAboutUnmockedScheduler, deferRenderPhaseUpdateToNextBatch, + decoupleUpdatePriorityFromScheduler, enableDebugTracing, } from 'shared/ReactFeatureFlags'; import ReactSharedInternals from 'shared/ReactSharedInternals'; @@ -124,6 +125,7 @@ import { InputDiscreteLanePriority, TransitionShortLanePriority, TransitionLongLanePriority, + DefaultLanePriority, NoLanes, NoLane, SyncLane, @@ -141,6 +143,8 @@ import { hasUpdatePriority, getNextLanes, returnNextLanesPriority, + setCurrentUpdateLanePriority, + getCurrentUpdateLanePriority, markStarvedLanesAsExpired, getLanesToRetrySynchronouslyOnError, markRootUpdated, @@ -405,7 +409,6 @@ export function requestUpdateLane( currentEventWipLanes = workInProgressRootIncludedLanes; } - let lane; if (suspenseConfig !== null) { // Use the size of the timeout as a heuristic to prioritize shorter // transitions over longer ones. @@ -423,26 +426,56 @@ export function requestUpdateLane( : NoLanes; } - lane = findTransitionLane( + return findTransitionLane( transitionLanePriority, currentEventWipLanes, currentEventPendingLanes, ); + } + + // TODO: Remove this dependency on the Scheduler priority. + // To do that, we're replacing it with an update lane priority. + const schedulerPriority = getCurrentPriorityLevel(); + + // The old behavior was using the priority level of the Scheduler. + // This couples React to the Scheduler internals, so we're replacing it + // with the currentUpdateLanePriority above. As an example of how this + // could be problematic, if we're not inside `Scheduler.runWithPriority`, + // then we'll get the priority of the current running Scheduler task, + // which is probably not what we want. + let lane; + if ( + // TODO: Temporary. We're removing the concept of discrete updates. + (executionContext & DiscreteEventContext) !== NoContext && + schedulerPriority === UserBlockingSchedulerPriority + ) { + lane = findUpdateLane(InputDiscreteLanePriority, currentEventWipLanes); } else { - // TODO: If we're not inside `runWithPriority`, this returns the priority - // of the currently running task. That's probably not what we want. - const schedulerPriority = getCurrentPriorityLevel(); + const schedulerLanePriority = schedulerPriorityToLanePriority( + schedulerPriority, + ); - if ( - // TODO: Temporary. We're removing the concept of discrete updates. - (executionContext & DiscreteEventContext) !== NoContext && - schedulerPriority === UserBlockingSchedulerPriority - ) { - lane = findUpdateLane(InputDiscreteLanePriority, currentEventWipLanes); - } else { - const lanePriority = schedulerPriorityToLanePriority(schedulerPriority); - lane = findUpdateLane(lanePriority, currentEventWipLanes); + if (decoupleUpdatePriorityFromScheduler) { + // In the new strategy, we will track the current update lane priority + // inside React and use that priority to select a lane for this update. + // For now, we're just logging when they're different so we can assess. + const currentUpdateLanePriority = getCurrentUpdateLanePriority(); + + if ( + schedulerLanePriority !== currentUpdateLanePriority && + currentUpdateLanePriority !== NoLanePriority + ) { + if (__DEV__) { + console.error( + 'Expected current scheduler lane priority %s to match current update lane priority %s', + schedulerLanePriority, + currentUpdateLanePriority, + ); + } + } } + + lane = findUpdateLane(schedulerLanePriority, currentEventWipLanes); } return lane; @@ -1079,7 +1112,13 @@ export function flushDiscreteUpdates() { export function deferredUpdates(fn: () => A): A { // TODO: Remove in favor of Scheduler.next - return runWithPriority(NormalSchedulerPriority, fn); + const previousLanePriority = getCurrentUpdateLanePriority(); + try { + setCurrentUpdateLanePriority(DefaultLanePriority); + return runWithPriority(NormalSchedulerPriority, fn); + } finally { + setCurrentUpdateLanePriority(previousLanePriority); + } } function flushPendingDiscreteUpdates() { @@ -1134,13 +1173,16 @@ export function discreteUpdates( ): R { const prevExecutionContext = executionContext; executionContext |= DiscreteEventContext; + const previousLanePriority = getCurrentUpdateLanePriority(); try { + setCurrentUpdateLanePriority(InputDiscreteLanePriority); // Should this return runWithPriority( UserBlockingSchedulerPriority, fn.bind(null, a, b, c, d), ); } finally { + setCurrentUpdateLanePriority(previousLanePriority); executionContext = prevExecutionContext; if (executionContext === NoContext) { // Flush the immediate callbacks that were scheduled during this batch @@ -1177,13 +1219,16 @@ export function flushSync(fn: A => R, a: A): R { return fn(a); } executionContext |= BatchedContext; + const previousLanePriority = getCurrentUpdateLanePriority(); try { + setCurrentUpdateLanePriority(SyncLanePriority); if (fn) { return runWithPriority(ImmediateSchedulerPriority, fn.bind(null, a)); } else { return (undefined: $FlowFixMe); } } finally { + setCurrentUpdateLanePriority(previousLanePriority); executionContext = prevExecutionContext; // Flush the immediate callbacks that were scheduled during this batch. // Note that this will happen even if batchedUpdates is higher up @@ -1195,9 +1240,12 @@ export function flushSync(fn: A => R, a: A): R { export function flushControlled(fn: () => mixed): void { const prevExecutionContext = executionContext; executionContext |= BatchedContext; + const previousLanePriority = getCurrentUpdateLanePriority(); try { + setCurrentUpdateLanePriority(SyncLanePriority); runWithPriority(ImmediateSchedulerPriority, fn); } finally { + setCurrentUpdateLanePriority(previousLanePriority); executionContext = prevExecutionContext; if (executionContext === NoContext) { // Flush the immediate callbacks that were scheduled during this batch @@ -1915,6 +1963,9 @@ function commitRootImpl(root, renderPriorityLevel) { } if (firstEffect !== null) { + const previousLanePriority = getCurrentUpdateLanePriority(); + setCurrentUpdateLanePriority(SyncLanePriority); + const prevExecutionContext = executionContext; executionContext |= CommitContext; const prevInteractions = pushInteractions(root); @@ -2035,6 +2086,9 @@ function commitRootImpl(root, renderPriorityLevel) { popInteractions(((prevInteractions: any): Set)); } executionContext = prevExecutionContext; + + // Reset the priority to the previous non-sync value. + setCurrentUpdateLanePriority(previousLanePriority); } else { // No effects. root.current = finishedWork; @@ -2321,7 +2375,15 @@ export function flushPassiveEffects() { ? NormalSchedulerPriority : pendingPassiveEffectsRenderPriority; pendingPassiveEffectsRenderPriority = NoSchedulerPriority; - return runWithPriority(priorityLevel, flushPassiveEffectsImpl); + const previousLanePriority = getCurrentUpdateLanePriority(); + try { + setCurrentUpdateLanePriority( + schedulerPriorityToLanePriority(priorityLevel), + ); + return runWithPriority(priorityLevel, flushPassiveEffectsImpl); + } finally { + setCurrentUpdateLanePriority(previousLanePriority); + } } } diff --git a/packages/react-reconciler/src/SchedulerWithReactIntegration.new.js b/packages/react-reconciler/src/SchedulerWithReactIntegration.new.js index 5618a9c78c96d..99780f6dbaff3 100644 --- a/packages/react-reconciler/src/SchedulerWithReactIntegration.new.js +++ b/packages/react-reconciler/src/SchedulerWithReactIntegration.new.js @@ -15,6 +15,11 @@ import * as Scheduler from 'scheduler'; import {__interactionsRef} from 'scheduler/tracing'; import {enableSchedulerTracing} from 'shared/ReactFeatureFlags'; import invariant from 'shared/invariant'; +import { + SyncLanePriority, + getCurrentUpdateLanePriority, + setCurrentUpdateLanePriority, +} from './ReactFiberLane'; const { unstable_runWithPriority: Scheduler_runWithPriority, @@ -171,9 +176,11 @@ function flushSyncCallbackQueueImpl() { // Prevent re-entrancy. isFlushingSyncQueue = true; let i = 0; + const previousLanePriority = getCurrentUpdateLanePriority(); try { const isSync = true; const queue = syncQueue; + setCurrentUpdateLanePriority(SyncLanePriority); runWithPriority(ImmediatePriority, () => { for (; i < queue.length; i++) { let callback = queue[i]; @@ -195,6 +202,7 @@ function flushSyncCallbackQueueImpl() { ); throw error; } finally { + setCurrentUpdateLanePriority(previousLanePriority); isFlushingSyncQueue = false; } } diff --git a/packages/react-reconciler/src/SchedulerWithReactIntegration.old.js b/packages/react-reconciler/src/SchedulerWithReactIntegration.old.js index 5618a9c78c96d..99780f6dbaff3 100644 --- a/packages/react-reconciler/src/SchedulerWithReactIntegration.old.js +++ b/packages/react-reconciler/src/SchedulerWithReactIntegration.old.js @@ -15,6 +15,11 @@ import * as Scheduler from 'scheduler'; import {__interactionsRef} from 'scheduler/tracing'; import {enableSchedulerTracing} from 'shared/ReactFeatureFlags'; import invariant from 'shared/invariant'; +import { + SyncLanePriority, + getCurrentUpdateLanePriority, + setCurrentUpdateLanePriority, +} from './ReactFiberLane'; const { unstable_runWithPriority: Scheduler_runWithPriority, @@ -171,9 +176,11 @@ function flushSyncCallbackQueueImpl() { // Prevent re-entrancy. isFlushingSyncQueue = true; let i = 0; + const previousLanePriority = getCurrentUpdateLanePriority(); try { const isSync = true; const queue = syncQueue; + setCurrentUpdateLanePriority(SyncLanePriority); runWithPriority(ImmediatePriority, () => { for (; i < queue.length; i++) { let callback = queue[i]; @@ -195,6 +202,7 @@ function flushSyncCallbackQueueImpl() { ); throw error; } finally { + setCurrentUpdateLanePriority(previousLanePriority); isFlushingSyncQueue = false; } } diff --git a/packages/react-reconciler/src/__tests__/ReactIncrementalUpdates-test.js b/packages/react-reconciler/src/__tests__/ReactIncrementalUpdates-test.js index d86c57a0c493f..ba363fb28bd98 100644 --- a/packages/react-reconciler/src/__tests__/ReactIncrementalUpdates-test.js +++ b/packages/react-reconciler/src/__tests__/ReactIncrementalUpdates-test.js @@ -516,11 +516,16 @@ describe('ReactIncrementalUpdates', () => { Scheduler.unstable_yieldValue('Committed: ' + log); if (log === 'B') { // Right after B commits, schedule additional updates. - Scheduler.unstable_runWithPriority( + // TODO: Double wrapping is temporary while we remove Scheduler runWithPriority. + ReactNoop.unstable_runWithPriority( Scheduler.unstable_UserBlockingPriority, - () => { - pushToLog('C'); - }, + () => + Scheduler.unstable_runWithPriority( + Scheduler.unstable_UserBlockingPriority, + () => { + pushToLog('C'); + }, + ), ); setLog(prevLog => prevLog + 'D'); } @@ -538,11 +543,17 @@ describe('ReactIncrementalUpdates', () => { await ReactNoop.act(async () => { pushToLog('A'); - Scheduler.unstable_runWithPriority( + + // TODO: Double wrapping is temporary while we remove Scheduler runWithPriority. + ReactNoop.unstable_runWithPriority( Scheduler.unstable_UserBlockingPriority, - () => { - pushToLog('B'); - }, + () => + Scheduler.unstable_runWithPriority( + Scheduler.unstable_UserBlockingPriority, + () => { + pushToLog('B'); + }, + ), ); }); expect(Scheduler).toHaveYielded([ @@ -574,11 +585,16 @@ describe('ReactIncrementalUpdates', () => { Scheduler.unstable_yieldValue('Committed: ' + this.state.log); if (this.state.log === 'B') { // Right after B commits, schedule additional updates. - Scheduler.unstable_runWithPriority( + // TODO: Double wrapping is temporary while we remove Scheduler runWithPriority. + ReactNoop.unstable_runWithPriority( Scheduler.unstable_UserBlockingPriority, - () => { - this.pushToLog('C'); - }, + () => + Scheduler.unstable_runWithPriority( + Scheduler.unstable_UserBlockingPriority, + () => { + this.pushToLog('C'); + }, + ), ); this.pushToLog('D'); } @@ -598,11 +614,16 @@ describe('ReactIncrementalUpdates', () => { await ReactNoop.act(async () => { pushToLog('A'); - Scheduler.unstable_runWithPriority( + // TODO: Double wrapping is temporary while we remove Scheduler runWithPriority. + ReactNoop.unstable_runWithPriority( Scheduler.unstable_UserBlockingPriority, - () => { - pushToLog('B'); - }, + () => + Scheduler.unstable_runWithPriority( + Scheduler.unstable_UserBlockingPriority, + () => { + pushToLog('B'); + }, + ), ); }); expect(Scheduler).toHaveYielded([ diff --git a/packages/react/src/__tests__/ReactDOMTracing-test.internal.js b/packages/react/src/__tests__/ReactDOMTracing-test.internal.js index 809753067383a..3259d5b70f7df 100644 --- a/packages/react/src/__tests__/ReactDOMTracing-test.internal.js +++ b/packages/react/src/__tests__/ReactDOMTracing-test.internal.js @@ -232,9 +232,14 @@ describe('ReactDOMTracing', () => { Scheduler.unstable_yieldValue('Child:update'); } else { Scheduler.unstable_yieldValue('Child:mount'); - Scheduler.unstable_runWithPriority( + // TODO: Double wrapping is temporary while we remove Scheduler runWithPriority. + ReactDOM.unstable_runWithPriority( Scheduler.unstable_IdlePriority, - () => setDidMount(true), + () => + Scheduler.unstable_runWithPriority( + Scheduler.unstable_IdlePriority, + () => setDidMount(true), + ), ); } }, [didMount]); diff --git a/packages/shared/ReactFeatureFlags.js b/packages/shared/ReactFeatureFlags.js index 88b6edb47c096..9e9ec4f75ad7c 100644 --- a/packages/shared/ReactFeatureFlags.js +++ b/packages/shared/ReactFeatureFlags.js @@ -120,3 +120,6 @@ export const enableLegacyFBSupport = false; // interleaved event. Remove this flag once we have migrated to the // new behavior. export const deferRenderPhaseUpdateToNextBatch = true; + +// Replacement for runWithPriority in React internals. +export const decoupleUpdatePriorityFromScheduler = false; diff --git a/packages/shared/forks/ReactFeatureFlags.native-fb.js b/packages/shared/forks/ReactFeatureFlags.native-fb.js index d88eeb41826e8..cbb47f8cf4b73 100644 --- a/packages/shared/forks/ReactFeatureFlags.native-fb.js +++ b/packages/shared/forks/ReactFeatureFlags.native-fb.js @@ -46,6 +46,7 @@ export const enableFilterEmptyStringAttributesDOM = false; export const enableNewReconciler = false; export const deferRenderPhaseUpdateToNextBatch = true; +export const decoupleUpdatePriorityFromScheduler = false; // Flow magic to verify the exports of this file match the original version. // eslint-disable-next-line no-unused-vars diff --git a/packages/shared/forks/ReactFeatureFlags.native-oss.js b/packages/shared/forks/ReactFeatureFlags.native-oss.js index 08ddd7d6cebd8..779e3379aac4b 100644 --- a/packages/shared/forks/ReactFeatureFlags.native-oss.js +++ b/packages/shared/forks/ReactFeatureFlags.native-oss.js @@ -45,6 +45,7 @@ export const enableFilterEmptyStringAttributesDOM = false; export const enableNewReconciler = false; export const deferRenderPhaseUpdateToNextBatch = true; +export const decoupleUpdatePriorityFromScheduler = false; // Flow magic to verify the exports of this file match the original version. // eslint-disable-next-line no-unused-vars diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.js index 867ae4fbe1d29..2837ff17dad5f 100644 --- a/packages/shared/forks/ReactFeatureFlags.test-renderer.js +++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.js @@ -45,6 +45,7 @@ export const enableFilterEmptyStringAttributesDOM = false; export const enableNewReconciler = false; export const deferRenderPhaseUpdateToNextBatch = true; +export const decoupleUpdatePriorityFromScheduler = false; // Flow magic to verify the exports of this file match the original version. // eslint-disable-next-line no-unused-vars diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js index 2672db4c488b6..5e036a4a17257 100644 --- a/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js +++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js @@ -45,6 +45,7 @@ export const enableFilterEmptyStringAttributesDOM = false; export const enableNewReconciler = false; export const deferRenderPhaseUpdateToNextBatch = true; +export const decoupleUpdatePriorityFromScheduler = false; // Flow magic to verify the exports of this file match the original version. // eslint-disable-next-line no-unused-vars diff --git a/packages/shared/forks/ReactFeatureFlags.testing.js b/packages/shared/forks/ReactFeatureFlags.testing.js index ed1e463cdd146..8f4930fedfd6d 100644 --- a/packages/shared/forks/ReactFeatureFlags.testing.js +++ b/packages/shared/forks/ReactFeatureFlags.testing.js @@ -45,6 +45,7 @@ export const enableFilterEmptyStringAttributesDOM = false; export const enableNewReconciler = false; export const deferRenderPhaseUpdateToNextBatch = true; +export const decoupleUpdatePriorityFromScheduler = false; // Flow magic to verify the exports of this file match the original version. // eslint-disable-next-line no-unused-vars diff --git a/packages/shared/forks/ReactFeatureFlags.testing.www.js b/packages/shared/forks/ReactFeatureFlags.testing.www.js index 3aaff5a548865..3f47f5a8f4acf 100644 --- a/packages/shared/forks/ReactFeatureFlags.testing.www.js +++ b/packages/shared/forks/ReactFeatureFlags.testing.www.js @@ -45,6 +45,7 @@ export const enableFilterEmptyStringAttributesDOM = false; export const enableNewReconciler = false; export const deferRenderPhaseUpdateToNextBatch = true; +export const decoupleUpdatePriorityFromScheduler = false; // Flow magic to verify the exports of this file match the original version. // eslint-disable-next-line no-unused-vars diff --git a/packages/shared/forks/ReactFeatureFlags.www-dynamic.js b/packages/shared/forks/ReactFeatureFlags.www-dynamic.js index 03100167f3f07..32e1cc15372d6 100644 --- a/packages/shared/forks/ReactFeatureFlags.www-dynamic.js +++ b/packages/shared/forks/ReactFeatureFlags.www-dynamic.js @@ -18,6 +18,7 @@ export const disableInputAttributeSyncing = __VARIANT__; export const enableFilterEmptyStringAttributesDOM = __VARIANT__; export const enableLegacyFBSupport = __VARIANT__; export const enableDebugTracing = !__VARIANT__; +export const decoupleUpdatePriorityFromScheduler = __VARIANT__; // This only has an effect in the new reconciler. But also, the new reconciler // is only enabled when __VARIANT__ is true. So this is set to the opposite of diff --git a/packages/shared/forks/ReactFeatureFlags.www.js b/packages/shared/forks/ReactFeatureFlags.www.js index 82d594872701f..15592c39095f5 100644 --- a/packages/shared/forks/ReactFeatureFlags.www.js +++ b/packages/shared/forks/ReactFeatureFlags.www.js @@ -75,6 +75,7 @@ export const warnUnstableRenderSubtreeIntoContainer = false; // don't have to add another test dimension. The build system will compile this // to the correct value. export const enableNewReconciler = __VARIANT__; +export const decoupleUpdatePriorityFromScheduler = __VARIANT__; // TODO: This does not currently exist in the new reconciler fork. export const enableDebugTracing = !__VARIANT__;