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