diff --git a/packages/react-reconciler/src/ReactFiberLane.new.js b/packages/react-reconciler/src/ReactFiberLane.new.js index ced32541ee8df..359afba7095e8 100644 --- a/packages/react-reconciler/src/ReactFiberLane.new.js +++ b/packages/react-reconciler/src/ReactFiberLane.new.js @@ -39,6 +39,7 @@ import { enableCache, enableSchedulingProfiler, enableUpdaterTracking, + enableSyncDefaultUpdates, } from 'shared/ReactFeatureFlags'; import {isDevToolsPresent} from './ReactFiberDevToolsHook.new'; @@ -273,6 +274,18 @@ export function getNextLanes(root: FiberRoot, wipLanes: Lanes): Lanes { } } + if ( + // TODO: Check for root override, once that lands + enableSyncDefaultUpdates && + (nextLanes & InputContinuousLane) !== NoLanes + ) { + // When updates are sync by default, we entangle continous priority updates + // and default updates, so they render in the same batch. The only reason + // they use separate lanes is because continuous updates should interrupt + // transitions, but default updates should not. + nextLanes |= pendingLanes & DefaultLane; + } + // Check for entangled lanes and add them to the batch. // // A lane is said to be entangled with another when it's not allowed to render diff --git a/packages/react-reconciler/src/ReactFiberLane.old.js b/packages/react-reconciler/src/ReactFiberLane.old.js index 5af142edbbae9..5db285a3fa24d 100644 --- a/packages/react-reconciler/src/ReactFiberLane.old.js +++ b/packages/react-reconciler/src/ReactFiberLane.old.js @@ -39,6 +39,7 @@ import { enableCache, enableSchedulingProfiler, enableUpdaterTracking, + enableSyncDefaultUpdates, } from 'shared/ReactFeatureFlags'; import {isDevToolsPresent} from './ReactFiberDevToolsHook.old'; @@ -273,6 +274,18 @@ export function getNextLanes(root: FiberRoot, wipLanes: Lanes): Lanes { } } + if ( + // TODO: Check for root override, once that lands + enableSyncDefaultUpdates && + (nextLanes & InputContinuousLane) !== NoLanes + ) { + // When updates are sync by default, we entangle continous priority updates + // and default updates, so they render in the same batch. The only reason + // they use separate lanes is because continuous updates should interrupt + // transitions, but default updates should not. + nextLanes |= pendingLanes & DefaultLane; + } + // Check for entangled lanes and add them to the batch. // // A lane is said to be entangled with another when it's not allowed to render diff --git a/packages/react-reconciler/src/ReactFiberWorkLoop.new.js b/packages/react-reconciler/src/ReactFiberWorkLoop.new.js index 665df80b13347..934f2697380ae 100644 --- a/packages/react-reconciler/src/ReactFiberWorkLoop.new.js +++ b/packages/react-reconciler/src/ReactFiberWorkLoop.new.js @@ -139,8 +139,8 @@ import { NoLanes, NoLane, SyncLane, - DefaultLane, DefaultHydrationLane, + DefaultLane, InputContinuousLane, InputContinuousHydrationLane, NoTimestamp, @@ -437,13 +437,6 @@ export function requestUpdateLane(fiber: Fiber): Lane { // TODO: Move this type conversion to the event priority module. const updateLane: Lane = (getCurrentUpdatePriority(): any); if (updateLane !== NoLane) { - if ( - enableSyncDefaultUpdates && - (updateLane === InputContinuousLane || - updateLane === InputContinuousHydrationLane) - ) { - return DefaultLane; - } return updateLane; } @@ -454,13 +447,6 @@ export function requestUpdateLane(fiber: Fiber): Lane { // use that directly. // TODO: Move this type conversion to the event priority module. const eventLane: Lane = (getCurrentEventPriority(): any); - if ( - enableSyncDefaultUpdates && - (eventLane === InputContinuousLane || - eventLane === InputContinuousHydrationLane) - ) { - return DefaultLane; - } return eventLane; } @@ -814,7 +800,9 @@ function performConcurrentWorkOnRoot(root, didTimeout) { let exitStatus = enableSyncDefaultUpdates && (includesSomeLane(lanes, DefaultLane) || - includesSomeLane(lanes, DefaultHydrationLane)) + includesSomeLane(lanes, InputContinuousLane) || + includesSomeLane(lanes, DefaultHydrationLane) || + includesSomeLane(lanes, InputContinuousHydrationLane)) ? // Time slicing is disabled for default updates in this root. renderRootSync(root, lanes) : renderRootConcurrent(root, lanes); diff --git a/packages/react-reconciler/src/ReactFiberWorkLoop.old.js b/packages/react-reconciler/src/ReactFiberWorkLoop.old.js index baebee3dc0490..97416c4b05641 100644 --- a/packages/react-reconciler/src/ReactFiberWorkLoop.old.js +++ b/packages/react-reconciler/src/ReactFiberWorkLoop.old.js @@ -139,8 +139,8 @@ import { NoLanes, NoLane, SyncLane, - DefaultLane, DefaultHydrationLane, + DefaultLane, InputContinuousLane, InputContinuousHydrationLane, NoTimestamp, @@ -437,13 +437,6 @@ export function requestUpdateLane(fiber: Fiber): Lane { // TODO: Move this type conversion to the event priority module. const updateLane: Lane = (getCurrentUpdatePriority(): any); if (updateLane !== NoLane) { - if ( - enableSyncDefaultUpdates && - (updateLane === InputContinuousLane || - updateLane === InputContinuousHydrationLane) - ) { - return DefaultLane; - } return updateLane; } @@ -454,13 +447,6 @@ export function requestUpdateLane(fiber: Fiber): Lane { // use that directly. // TODO: Move this type conversion to the event priority module. const eventLane: Lane = (getCurrentEventPriority(): any); - if ( - enableSyncDefaultUpdates && - (eventLane === InputContinuousLane || - eventLane === InputContinuousHydrationLane) - ) { - return DefaultLane; - } return eventLane; } @@ -814,7 +800,9 @@ function performConcurrentWorkOnRoot(root, didTimeout) { let exitStatus = enableSyncDefaultUpdates && (includesSomeLane(lanes, DefaultLane) || - includesSomeLane(lanes, DefaultHydrationLane)) + includesSomeLane(lanes, InputContinuousLane) || + includesSomeLane(lanes, DefaultHydrationLane) || + includesSomeLane(lanes, InputContinuousHydrationLane)) ? // Time slicing is disabled for default updates in this root. renderRootSync(root, lanes) : renderRootConcurrent(root, lanes); diff --git a/packages/react-reconciler/src/__tests__/ReactHooksWithNoopRenderer-test.js b/packages/react-reconciler/src/__tests__/ReactHooksWithNoopRenderer-test.js index 485d477ed7e8e..07d3f7ff526aa 100644 --- a/packages/react-reconciler/src/__tests__/ReactHooksWithNoopRenderer-test.js +++ b/packages/react-reconciler/src/__tests__/ReactHooksWithNoopRenderer-test.js @@ -1405,18 +1405,6 @@ describe('ReactHooksWithNoopRenderer', () => { ReactNoop.unstable_runWithPriority(ContinuousEventPriority, () => { setParentState(false); }); - if (gate(flags => flags.enableSyncDefaultUpdates)) { - // TODO: Default updates do not interrupt transition updates, to - // prevent starvation. However, when sync default updates are enabled, - // continuous updates are treated like default updates. In this case, - // we probably don't want this behavior; continuous should be allowed - // to interrupt. - expect(Scheduler).toFlushUntilNextPaint([ - 'Child two render', - 'Child one commit', - 'Child two commit', - ]); - } expect(Scheduler).toFlushUntilNextPaint([ 'Parent false render', 'Parent false commit', diff --git a/packages/react-reconciler/src/__tests__/ReactUpdatePriority-test.js b/packages/react-reconciler/src/__tests__/ReactUpdatePriority-test.js index 0aee7df66df4a..116cc4c4d8e00 100644 --- a/packages/react-reconciler/src/__tests__/ReactUpdatePriority-test.js +++ b/packages/react-reconciler/src/__tests__/ReactUpdatePriority-test.js @@ -1,6 +1,8 @@ let React; let ReactNoop; let Scheduler; +let ContinuousEventPriority; +let startTransition; let useState; let useEffect; @@ -11,6 +13,9 @@ describe('ReactUpdatePriority', () => { React = require('react'); ReactNoop = require('react-noop-renderer'); Scheduler = require('scheduler'); + ContinuousEventPriority = require('react-reconciler/constants') + .ContinuousEventPriority; + startTransition = React.unstable_startTransition; useState = React.useState; useEffect = React.useEffect; }); @@ -78,4 +83,53 @@ describe('ReactUpdatePriority', () => { // Now the idle update has flushed expect(Scheduler).toHaveYielded(['Idle: 2, Default: 2']); }); + + // @gate experimental + test('continuous updates should interrupt transisions', async () => { + const root = ReactNoop.createRoot(); + + let setCounter; + let setIsHidden; + function App() { + const [counter, _setCounter] = useState(1); + const [isHidden, _setIsHidden] = useState(false); + setCounter = _setCounter; + setIsHidden = _setIsHidden; + if (isHidden) { + return ; + } + return ( + <> + + + + + ); + } + + await ReactNoop.act(async () => { + root.render(); + }); + expect(Scheduler).toHaveYielded(['A1', 'B1', 'C1']); + expect(root).toMatchRenderedOutput('A1B1C1'); + + await ReactNoop.act(async () => { + startTransition(() => { + setCounter(2); + }); + expect(Scheduler).toFlushAndYieldThrough(['A2']); + ReactNoop.unstable_runWithPriority(ContinuousEventPriority, () => { + setIsHidden(true); + }); + }); + expect(Scheduler).toHaveYielded([ + // Because the hide update has continous priority, it should interrupt the + // in-progress transition + '(hidden)', + // When the transition resumes, it's a no-op because the children are + // now hidden. + '(hidden)', + ]); + expect(root).toMatchRenderedOutput('(hidden)'); + }); }); diff --git a/packages/react-reconciler/src/__tests__/SchedulingProfilerLabels-test.internal.js b/packages/react-reconciler/src/__tests__/SchedulingProfilerLabels-test.internal.js index da59c153f43dd..6474be855b38f 100644 --- a/packages/react-reconciler/src/__tests__/SchedulingProfilerLabels-test.internal.js +++ b/packages/react-reconciler/src/__tests__/SchedulingProfilerLabels-test.internal.js @@ -168,18 +168,10 @@ describe('SchedulingProfiler labels', () => { event.initEvent('mouseover', true, true); dispatchAndSetCurrentEvent(targetRef.current, event); }); - if (gate(flags => flags.enableSyncDefaultUpdates)) { - expect(clearedMarks).toContain( - `--schedule-state-update-${formatLanes( - ReactFiberLane.DefaultLane, - )}-App`, - ); - } else { - expect(clearedMarks).toContain( - `--schedule-state-update-${formatLanes( - ReactFiberLane.InputContinuousLane, - )}-App`, - ); - } + expect(clearedMarks).toContain( + `--schedule-state-update-${formatLanes( + ReactFiberLane.InputContinuousLane, + )}-App`, + ); }); });