diff --git a/packages/react-reconciler/src/ReactFiberWorkLoop.js b/packages/react-reconciler/src/ReactFiberWorkLoop.js index 84b27db051c5..dd76b3aed979 100644 --- a/packages/react-reconciler/src/ReactFiberWorkLoop.js +++ b/packages/react-reconciler/src/ReactFiberWorkLoop.js @@ -323,6 +323,9 @@ export function computeExpirationForFiber( // If we're in the middle of rendering a tree, do not update at the same // expiration time that is already rendering. + // TODO: We shouldn't have to do this if the update is on a different root. + // Refactor computeExpirationForFiber + scheduleUpdate so we have access to + // the root when we check for this condition. if (workInProgressRoot !== null && expirationTime === renderExpirationTime) { // This is a trick to move this update into a separate batch expirationTime -= 1; @@ -806,8 +809,10 @@ function renderRoot( return null; } - if (root.finishedExpirationTime === expirationTime) { + if (isSync && root.finishedExpirationTime === expirationTime) { // There's already a pending commit at this expiration time. + // TODO: This is poorly factored. This case only exists for the + // batch.commit() API. return commitRoot.bind(null, root); } diff --git a/packages/react-reconciler/src/__tests__/ReactSuspenseWithNoopRenderer-test.internal.js b/packages/react-reconciler/src/__tests__/ReactSuspenseWithNoopRenderer-test.internal.js index a42136a80b90..c1fab933daa3 100644 --- a/packages/react-reconciler/src/__tests__/ReactSuspenseWithNoopRenderer-test.internal.js +++ b/packages/react-reconciler/src/__tests__/ReactSuspenseWithNoopRenderer-test.internal.js @@ -2294,4 +2294,51 @@ describe('ReactSuspenseWithNoopRenderer', () => { expect(ReactNoop.getChildren()).toEqual([span('D')]); }); }); + + it("suspended commit remains suspended even if there's another update at same expiration", async () => { + // Regression test + function App({text}) { + return ( + + + + ); + } + + const root = ReactNoop.createRoot(); + await ReactNoop.act(async () => { + root.render(); + }); + + // Resolve initial render + await ReactNoop.act(async () => { + Scheduler.advanceTime(2000); + await advanceTimers(2000); + }); + expect(Scheduler).toHaveYielded([ + 'Suspend! [Initial]', + 'Promise resolved [Initial]', + 'Initial', + ]); + expect(root).toMatchRenderedOutput(); + + // Suspend B. Since showing a fallback would hide content that's already + // visible, it should suspend for a bit without committing. + await ReactNoop.act(async () => { + root.render(); + + expect(Scheduler).toFlushAndYield(['Suspend! [First update]']); + // Should not display a fallback + expect(root).toMatchRenderedOutput(); + }); + + // Suspend A. This should also suspend for a JND. + await ReactNoop.act(async () => { + root.render(); + + expect(Scheduler).toFlushAndYield(['Suspend! [Second update]']); + // Should not display a fallback + expect(root).toMatchRenderedOutput(); + }); + }); });