diff --git a/packages/react-reconciler/src/ReactFiberScheduler.new.js b/packages/react-reconciler/src/ReactFiberScheduler.new.js index e2b4517de23fe..c86bd58ce6a57 100644 --- a/packages/react-reconciler/src/ReactFiberScheduler.new.js +++ b/packages/react-reconciler/src/ReactFiberScheduler.new.js @@ -742,26 +742,38 @@ function renderRoot( } startWorkLoopTimer(workInProgress); + + // TODO: Fork renderRoot into renderRootSync and renderRootAsync + if (isSync) { + if (expirationTime !== Sync) { + // An async update expired. There may be other expired updates on + // this root. We should render all the expired work in a + // single batch. + const currentTime = requestCurrentTime(); + if (currentTime < expirationTime) { + // Restart at the current time. + workPhase = prevWorkPhase; + resetContextDependencies(); + ReactCurrentDispatcher.current = prevDispatcher; + if (enableSchedulerTracing) { + __interactionsRef.current = ((prevInteractions: any): Set< + Interaction, + >); + } + return renderRoot.bind(null, root, currentTime); + } + } + } else { + // Since we know we're in a React event, we can clear the current + // event time. The next update will compute a new event time. + currentEventTime = NoWork; + } + do { try { if (isSync) { - if (expirationTime !== Sync) { - // An async update expired. There may be other expired updates on - // this root. We should render all the expired work in a - // single batch. - const currentTime = requestCurrentTime(); - if (currentTime < expirationTime) { - // Restart at the current time. - workPhase = prevWorkPhase; - ReactCurrentDispatcher.current = prevDispatcher; - return renderRoot.bind(null, root, currentTime); - } - } workLoopSync(); } else { - // Since we know we're in a React event, we can clear the current - // event time. The next update will compute a new event time. - currentEventTime = NoWork; workLoop(); } break; diff --git a/packages/react-reconciler/src/__tests__/ReactSuspenseWithNoopRenderer-test.internal.js b/packages/react-reconciler/src/__tests__/ReactSuspenseWithNoopRenderer-test.internal.js index b68b239f80e68..89f3f2a698c62 100644 --- a/packages/react-reconciler/src/__tests__/ReactSuspenseWithNoopRenderer-test.internal.js +++ b/packages/react-reconciler/src/__tests__/ReactSuspenseWithNoopRenderer-test.internal.js @@ -827,6 +827,50 @@ describe('ReactSuspenseWithNoopRenderer', () => { expect(ReactNoop.getChildren()).toEqual([span('goodbye')]); }); + it('a suspended update that expires', async () => { + // Regression test. This test used to fall into an infinite loop. + function ExpensiveText({text}) { + // This causes the update to expire. + Scheduler.advanceTime(10000); + // Then something suspends. + return ; + } + + function App() { + return ( + + + + + + ); + } + + ReactNoop.render(); + expect(Scheduler).toFlushAndYield([ + 'Suspend! [A]', + 'Suspend! [B]', + 'Suspend! [C]', + ]); + expect(ReactNoop).toMatchRenderedOutput('Loading...'); + + await advanceTimers(200000); + expect(Scheduler).toHaveYielded([ + 'Promise resolved [A]', + 'Promise resolved [B]', + 'Promise resolved [C]', + ]); + + expect(Scheduler).toFlushAndYield(['A', 'B', 'C']); + expect(ReactNoop).toMatchRenderedOutput( + + + + + , + ); + }); + describe('sync mode', () => { it('times out immediately', async () => { function App() {