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(
-
-
-
-
- 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');
});
+
- ,
- );
- 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(