diff --git a/packages/react-reconciler/src/ReactFiberInterleavedUpdates.new.js b/packages/react-reconciler/src/ReactFiberInterleavedUpdates.new.js index 65460c7658ab..010730b1e78e 100644 --- a/packages/react-reconciler/src/ReactFiberInterleavedUpdates.new.js +++ b/packages/react-reconciler/src/ReactFiberInterleavedUpdates.new.js @@ -28,6 +28,10 @@ export function pushInterleavedQueue( } } +export function hasInterleavedUpdates() { + return interleavedQueues !== null; +} + export function enqueueInterleavedUpdates() { // Transfer the interleaved updates onto the main queue. Each queue has a // `pending` field and an `interleaved` field. When they are not null, they diff --git a/packages/react-reconciler/src/ReactFiberInterleavedUpdates.old.js b/packages/react-reconciler/src/ReactFiberInterleavedUpdates.old.js index 5fd769684ebd..0d3319801daa 100644 --- a/packages/react-reconciler/src/ReactFiberInterleavedUpdates.old.js +++ b/packages/react-reconciler/src/ReactFiberInterleavedUpdates.old.js @@ -28,6 +28,10 @@ export function pushInterleavedQueue( } } +export function hasInterleavedUpdates() { + return interleavedQueues !== null; +} + export function enqueueInterleavedUpdates() { // Transfer the interleaved updates onto the main queue. Each queue has a // `pending` field and an `interleaved` field. When they are not null, they diff --git a/packages/react-reconciler/src/ReactFiberWorkLoop.new.js b/packages/react-reconciler/src/ReactFiberWorkLoop.new.js index b6e7fad4eb94..3f0d3278f488 100644 --- a/packages/react-reconciler/src/ReactFiberWorkLoop.new.js +++ b/packages/react-reconciler/src/ReactFiberWorkLoop.new.js @@ -195,7 +195,10 @@ import { pop as popFromStack, createCursor, } from './ReactFiberStack.new'; -import {enqueueInterleavedUpdates} from './ReactFiberInterleavedUpdates.new'; +import { + enqueueInterleavedUpdates, + hasInterleavedUpdates, +} from './ReactFiberInterleavedUpdates.new'; import { markNestedUpdateScheduled, @@ -723,7 +726,13 @@ export function isInterleavedUpdate(fiber: Fiber, lane: Lane) { // TODO: Optimize slightly by comparing to root that fiber belongs to. // Requires some refactoring. Not a big deal though since it's rare for // concurrent apps to have more than a single root. - workInProgressRoot !== null && + (workInProgressRoot !== null || + // If the interleaved updates queue hasn't been cleared yet, then + // we should treat this as an interleaved update, too. This is also a + // defensive coding measure in case a new update comes in between when + // rendering has finished and when the interleaved updates are transferred + // to the main queue. + hasInterleavedUpdates() !== null) && (fiber.mode & ConcurrentMode) !== NoMode && // If this is a render phase update (i.e. UNSAFE_componentWillReceiveProps), // then don't treat this as an interleaved update. This pattern is diff --git a/packages/react-reconciler/src/ReactFiberWorkLoop.old.js b/packages/react-reconciler/src/ReactFiberWorkLoop.old.js index 62714c8cd334..7dbbc9b25f9a 100644 --- a/packages/react-reconciler/src/ReactFiberWorkLoop.old.js +++ b/packages/react-reconciler/src/ReactFiberWorkLoop.old.js @@ -195,7 +195,10 @@ import { pop as popFromStack, createCursor, } from './ReactFiberStack.old'; -import {enqueueInterleavedUpdates} from './ReactFiberInterleavedUpdates.old'; +import { + enqueueInterleavedUpdates, + hasInterleavedUpdates, +} from './ReactFiberInterleavedUpdates.old'; import { markNestedUpdateScheduled, @@ -723,7 +726,13 @@ export function isInterleavedUpdate(fiber: Fiber, lane: Lane) { // TODO: Optimize slightly by comparing to root that fiber belongs to. // Requires some refactoring. Not a big deal though since it's rare for // concurrent apps to have more than a single root. - workInProgressRoot !== null && + (workInProgressRoot !== null || + // If the interleaved updates queue hasn't been cleared yet, then + // we should treat this as an interleaved update, too. This is also a + // defensive coding measure in case a new update comes in between when + // rendering has finished and when the interleaved updates are transferred + // to the main queue. + hasInterleavedUpdates() !== null) && (fiber.mode & ConcurrentMode) !== NoMode && // If this is a render phase update (i.e. UNSAFE_componentWillReceiveProps), // then don't treat this as an interleaved update. This pattern is diff --git a/packages/react-reconciler/src/__tests__/ReactInterleavedUpdates-test.js b/packages/react-reconciler/src/__tests__/ReactInterleavedUpdates-test.js index c9e66fe0399e..153d4d28bd3f 100644 --- a/packages/react-reconciler/src/__tests__/ReactInterleavedUpdates-test.js +++ b/packages/react-reconciler/src/__tests__/ReactInterleavedUpdates-test.js @@ -140,4 +140,52 @@ describe('ReactInterleavedUpdates', () => { expect(Scheduler).toHaveYielded([2, 2, 2]); expect(root).toMatchRenderedOutput('222'); }); + + test('regression for #24350: does not add to main update queue until interleaved update queue has been cleared', async () => { + let setStep; + function App() { + const [step, _setState] = useState(0); + setStep = _setState; + return ( + <> + + + + + ); + } + + const root = ReactNoop.createRoot(); + await act(async () => { + root.render(); + }); + expect(Scheduler).toHaveYielded(['A0', 'B0', 'C0']); + expect(root).toMatchRenderedOutput('A0B0C0'); + + await act(async () => { + // Start the render phase. + startTransition(() => { + setStep(1); + }); + expect(Scheduler).toFlushAndYieldThrough(['A1', 'B1']); + + // Schedule an interleaved update. This gets placed on a special queue. + startTransition(() => { + setStep(2); + }); + + // Finish rendering the first update. + expect(Scheduler).toFlushUntilNextPaint(['C1']); + + // Schedule another update. (In the regression case, this was treated + // as a normal, non-interleaved update and it was inserted into the queue + // before the interleaved one was processed.) + startTransition(() => { + setStep(3); + }); + }); + // The last update should win. + expect(Scheduler).toHaveYielded(['A3', 'B3', 'C3']); + expect(root).toMatchRenderedOutput('A3B3C3'); + }); });