diff --git a/packages/react-reconciler/src/ReactFiberLane.new.js b/packages/react-reconciler/src/ReactFiberLane.new.js
index adb67b6e77dfe..1a3c116b6e7d0 100644
--- a/packages/react-reconciler/src/ReactFiberLane.new.js
+++ b/packages/react-reconciler/src/ReactFiberLane.new.js
@@ -327,7 +327,16 @@ export function getNextLanes(root: FiberRoot, wipLanes: Lanes): Lanes {
) {
getHighestPriorityLanes(wipLanes);
const wipLanePriority = return_highestLanePriority;
- if (nextLanePriority <= wipLanePriority) {
+ if (
+ nextLanePriority <= wipLanePriority ||
+ // Default priority updates should not interrupt transition updates. The
+ // only difference between default updates and transition updates is that
+ // default updates do not support refresh transitions.
+ (enableTransitionEntanglement &&
+ nextLanePriority === DefaultLanePriority &&
+ wipLanePriority === TransitionPriority)
+ ) {
+ // Keep working on the existing in-progress tree. Do not interrupt.
return wipLanes;
} else {
return_highestLanePriority = nextLanePriority;
diff --git a/packages/react-reconciler/src/ReactFiberLane.old.js b/packages/react-reconciler/src/ReactFiberLane.old.js
index ea02f3102c902..1d5eb7fe1c2a6 100644
--- a/packages/react-reconciler/src/ReactFiberLane.old.js
+++ b/packages/react-reconciler/src/ReactFiberLane.old.js
@@ -327,7 +327,16 @@ export function getNextLanes(root: FiberRoot, wipLanes: Lanes): Lanes {
) {
getHighestPriorityLanes(wipLanes);
const wipLanePriority = return_highestLanePriority;
- if (nextLanePriority <= wipLanePriority) {
+ if (
+ nextLanePriority <= wipLanePriority ||
+ // Default priority updates should not interrupt transition updates. The
+ // only difference between default updates and transition updates is that
+ // default updates do not support refresh transitions.
+ (enableTransitionEntanglement &&
+ nextLanePriority === DefaultLanePriority &&
+ wipLanePriority === TransitionPriority)
+ ) {
+ // Keep working on the existing in-progress tree. Do not interrupt.
return wipLanes;
} else {
return_highestLanePriority = nextLanePriority;
diff --git a/packages/react-reconciler/src/__tests__/ReactIncrementalErrorHandling-test.internal.js b/packages/react-reconciler/src/__tests__/ReactIncrementalErrorHandling-test.internal.js
index 4cdb4410ba592..f568191681b67 100644
--- a/packages/react-reconciler/src/__tests__/ReactIncrementalErrorHandling-test.internal.js
+++ b/packages/react-reconciler/src/__tests__/ReactIncrementalErrorHandling-test.internal.js
@@ -1849,10 +1849,12 @@ describe('ReactIncrementalErrorHandling', () => {
// the queue.
expect(Scheduler).toFlushAndYieldThrough(['Everything is fine.']);
- // Schedule a default pri update on a child that triggers an error.
+ // Schedule a discrete update on a child that triggers an error.
// The root should capture this error. But since there's still a pending
// update on the root, the error should be suppressed.
- setShouldThrow(true);
+ ReactNoop.discreteUpdates(() => {
+ setShouldThrow(true);
+ });
});
// Should render the final state without throwing the error.
expect(Scheduler).toHaveYielded(['Everything is fine.']);
diff --git a/packages/react-reconciler/src/__tests__/ReactTransition-test.js b/packages/react-reconciler/src/__tests__/ReactTransition-test.js
index ffd2c9e19ebc7..e6aa258bcd59b 100644
--- a/packages/react-reconciler/src/__tests__/ReactTransition-test.js
+++ b/packages/react-reconciler/src/__tests__/ReactTransition-test.js
@@ -15,6 +15,7 @@ let ReactNoop;
let Scheduler;
let Suspense;
let useState;
+let useLayoutEffect;
let useTransition;
let startTransition;
let act;
@@ -30,6 +31,7 @@ describe('ReactTransition', () => {
ReactNoop = require('react-noop-renderer');
Scheduler = require('scheduler');
useState = React.useState;
+ useLayoutEffect = React.useLayoutEffect;
useTransition = React.unstable_useTransition;
Suspense = React.Suspense;
startTransition = React.unstable_startTransition;
@@ -773,4 +775,187 @@ describe('ReactTransition', () => {
});
},
);
+
+ // @gate experimental
+ // @gate enableCache
+ it('should render normal pri updates scheduled after transitions before transitions', async () => {
+ let updateTransitionPri;
+ let updateNormalPri;
+ function App() {
+ const [normalPri, setNormalPri] = useState(0);
+ const [transitionPri, setTransitionPri] = useState(0);
+ updateTransitionPri = () =>
+ startTransition(() => setTransitionPri(n => n + 1));
+ updateNormalPri = () => setNormalPri(n => n + 1);
+
+ useLayoutEffect(() => {
+ Scheduler.unstable_yieldValue('Commit');
+ });
+
+ return (
+ }>
+
+ {', '}
+
+
+ );
+ }
+
+ const root = ReactNoop.createRoot();
+ await act(async () => {
+ root.render();
+ });
+
+ // Initial render.
+ expect(Scheduler).toHaveYielded([
+ 'Transition pri: 0',
+ 'Normal pri: 0',
+ 'Commit',
+ ]);
+ expect(root).toMatchRenderedOutput('Transition pri: 0, Normal pri: 0');
+
+ await act(async () => {
+ updateTransitionPri();
+ updateNormalPri();
+ });
+
+ expect(Scheduler).toHaveYielded([
+ // Normal update first.
+ 'Transition pri: 0',
+ 'Normal pri: 1',
+ 'Commit',
+
+ // Then transition update.
+ 'Transition pri: 1',
+ 'Normal pri: 1',
+ 'Commit',
+ ]);
+ expect(root).toMatchRenderedOutput('Transition pri: 1, Normal pri: 1');
+ });
+
+ // @gate experimental
+ // @gate enableCache
+ it('should render normal pri updates before transition suspense retries', async () => {
+ let updateTransitionPri;
+ let updateNormalPri;
+ function App() {
+ const [transitionPri, setTransitionPri] = useState(false);
+ const [normalPri, setNormalPri] = useState(0);
+
+ updateTransitionPri = () => startTransition(() => setTransitionPri(true));
+ updateNormalPri = () => setNormalPri(n => n + 1);
+
+ useLayoutEffect(() => {
+ Scheduler.unstable_yieldValue('Commit');
+ });
+
+ return (
+ }>
+ {transitionPri ? : }
+ {', '}
+
+
+ );
+ }
+
+ const root = ReactNoop.createRoot();
+ await act(async () => {
+ root.render();
+ });
+
+ // Initial render.
+ expect(Scheduler).toHaveYielded(['(empty)', 'Normal pri: 0', 'Commit']);
+ expect(root).toMatchRenderedOutput('(empty), Normal pri: 0');
+
+ await act(async () => {
+ updateTransitionPri();
+ });
+
+ expect(Scheduler).toHaveYielded([
+ // Suspend.
+ 'Suspend! [Async]',
+ 'Normal pri: 0',
+ 'Loading...',
+ ]);
+ expect(root).toMatchRenderedOutput('(empty), Normal pri: 0');
+
+ await act(async () => {
+ await resolveText('Async');
+ updateNormalPri();
+ });
+
+ expect(Scheduler).toHaveYielded([
+ // Normal pri update.
+ '(empty)',
+ 'Normal pri: 1',
+ 'Commit',
+
+ // Promise resolved, retry flushed.
+ 'Async',
+ 'Normal pri: 1',
+ 'Commit',
+ ]);
+ expect(root).toMatchRenderedOutput('Async, Normal pri: 1');
+ });
+
+ // @gate experimental
+ // @gate enableCache
+ it('should not interrupt transitions with normal pri updates', async () => {
+ let updateNormalPri;
+ let updateTransitionPri;
+ function App() {
+ const [transitionPri, setTransitionPri] = useState(0);
+ const [normalPri, setNormalPri] = useState(0);
+ updateTransitionPri = () =>
+ startTransition(() => setTransitionPri(n => n + 1));
+ updateNormalPri = () => setNormalPri(n => n + 1);
+
+ useLayoutEffect(() => {
+ Scheduler.unstable_yieldValue('Commit');
+ });
+ return (
+ <>
+
+ {', '}
+
+ >
+ );
+ }
+
+ const root = ReactNoop.createRoot();
+ await ReactNoop.act(async () => {
+ root.render();
+ });
+ expect(Scheduler).toHaveYielded([
+ 'Transition pri: 0',
+ 'Normal pri: 0',
+ 'Commit',
+ ]);
+ expect(root).toMatchRenderedOutput('Transition pri: 0, Normal pri: 0');
+
+ await ReactNoop.act(async () => {
+ updateTransitionPri();
+
+ expect(Scheduler).toFlushAndYieldThrough([
+ // Start transition update.
+ 'Transition pri: 1',
+ ]);
+
+ // Schedule normal pri update during transition update.
+ // This should not interrupt.
+ updateNormalPri();
+ });
+
+ expect(Scheduler).toHaveYielded([
+ // Finish transition update.
+ 'Normal pri: 0',
+ 'Commit',
+
+ // Normal pri update.
+ 'Transition pri: 1',
+ 'Normal pri: 1',
+ 'Commit',
+ ]);
+ expect(root).toMatchRenderedOutput('Transition pri: 1, Normal pri: 1');
+ });
});