diff --git a/packages/react-dom/src/__tests__/ReactDOMFloat-test.js b/packages/react-dom/src/__tests__/ReactDOMFloat-test.js index 5bdca5d43f1f8..d089b25ffad03 100644 --- a/packages/react-dom/src/__tests__/ReactDOMFloat-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMFloat-test.js @@ -3216,7 +3216,7 @@ body { ); }); - it('can start a new suspended commit after a previous one finishes', async () => { + it('can interrupt a suspended commit with a new transition', async () => { function App({children}) { return ( @@ -3225,81 +3225,66 @@ body { ); } const root = ReactDOMClient.createRoot(document); - root.render(); + root.render((empty)); + + // Start a transition to "A" React.startTransition(() => { root.render( - hello - + A + , ); }); await waitForAll([]); + + // "A" hasn't loaded yet, so we remain on the initial UI. Its preload + // has been inserted into the head, though. expect(getMeaningfulChildren(document)).toEqual( - + - + (empty) , ); + // Interrupt the "A" transition with a new one, "B" React.startTransition(() => { root.render( - hello2 - {null} - + B + , ); }); await waitForAll([]); - expect(getMeaningfulChildren(document)).toEqual( - - - - - - , - ); - loadPreloads(); - loadStylesheets(); - assertLog(['load preload: foo', 'load stylesheet: foo']); + // Still on the initial UI because "B" hasn't loaded, but its preload + // is now in the head, too. expect(getMeaningfulChildren(document)).toEqual( - - + + - hello + (empty) , ); - // The second update should process now - await waitForAll([]); - expect(getMeaningfulChildren(document)).toEqual( - - - - - - - hello - , - ); + // Finish loading loadPreloads(); loadStylesheets(); - assertLog(['load preload: bar', 'load stylesheet: bar']); + assertLog(['load preload: A', 'load preload: B', 'load stylesheet: B']); + // The "B" transition has finished. expect(getMeaningfulChildren(document)).toEqual( - - - - + + + - hello2 + B , ); }); diff --git a/packages/react-reconciler/src/ReactFiberRootScheduler.js b/packages/react-reconciler/src/ReactFiberRootScheduler.js index ce38c203d2295..db60ab8be5360 100644 --- a/packages/react-reconciler/src/ReactFiberRootScheduler.js +++ b/packages/react-reconciler/src/ReactFiberRootScheduler.js @@ -18,7 +18,6 @@ import { SyncLane, getHighestPriorityLane, getNextLanes, - includesOnlyNonUrgentLanes, includesSyncLane, markStarvedLanesAsExpired, } from './ReactFiberLane'; @@ -301,14 +300,16 @@ function scheduleTaskForRootDuringMicrotask( const existingCallbackNode = root.callbackNode; if ( + // Check if there's nothing to work on 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)) + // + // Suspended render phase + (root === workInProgressRoot && isWorkLoopSuspendedOnData()) || + // Suspended commit phase + root.cancelPendingCommit !== null ) { // Fast path: There's nothing to work on. if (existingCallbackNode !== null) { diff --git a/packages/react-reconciler/src/ReactFiberWorkLoop.js b/packages/react-reconciler/src/ReactFiberWorkLoop.js index c32cf10e0d8f3..292cad9d0735e 100644 --- a/packages/react-reconciler/src/ReactFiberWorkLoop.js +++ b/packages/react-reconciler/src/ReactFiberWorkLoop.js @@ -725,8 +725,11 @@ export function scheduleUpdateOnFiber( // Check if the work loop is currently suspended and waiting for data to // finish loading. if ( - workInProgressSuspendedReason === SuspendedOnData && - root === workInProgressRoot + // Suspended render phase + (root === workInProgressRoot && + workInProgressSuspendedReason === SuspendedOnData) || + // Suspended commit phase + root.cancelPendingCommit !== null ) { // The incoming update might unblock the current render. Interrupt the // current attempt and restart from the top. diff --git a/packages/react-reconciler/src/__tests__/ReactSuspenseyCommitPhase-test.js b/packages/react-reconciler/src/__tests__/ReactSuspenseyCommitPhase-test.js index bbd2ae8c2edcb..2a558ba93a523 100644 --- a/packages/react-reconciler/src/__tests__/ReactSuspenseyCommitPhase-test.js +++ b/packages/react-reconciler/src/__tests__/ReactSuspenseyCommitPhase-test.js @@ -137,7 +137,7 @@ describe('ReactSuspenseyCommitPhase', () => { // Nothing showing yet. expect(root).toMatchRenderedOutput(null); - // If there's an urgent update, it should interrupt the suspended commit. + // If there's an update, it should interrupt the suspended commit. await act(() => { root.render(); }); @@ -145,7 +145,7 @@ describe('ReactSuspenseyCommitPhase', () => { expect(root).toMatchRenderedOutput('Something else'); }); - test('a non-urgent update does not interrupt a suspended commit', async () => { + test('a transition update interrupts a suspended commit', async () => { const root = ReactNoop.createRoot(); // Mount an image. This transition will suspend because it's not inside a @@ -159,26 +159,12 @@ describe('ReactSuspenseyCommitPhase', () => { // Nothing showing yet. expect(root).toMatchRenderedOutput(null); - // If there's another transition update, it should not interrupt the - // suspended commit. + // If there's an update, it should interrupt the suspended commit. await act(() => { startTransition(() => { root.render(); }); }); - // Still suspended. - expect(root).toMatchRenderedOutput(null); - - await act(() => { - // Resolving the image should result in an immediate, synchronous commit. - resolveSuspenseyThing('A'); - expect(root).toMatchRenderedOutput(); - }); - // Then the second transition is unblocked. - // TODO: Right now the only way to unsuspend a commit early is to proceed - // with the commit even if everything isn't ready. Maybe there should also - // be a way to abort a commit so that it can be interrupted by - // another transition. assertLog(['Something else']); expect(root).toMatchRenderedOutput('Something else'); });