diff --git a/packages/react-reconciler/src/__tests__/ReactSuspenseWithNoopRenderer-test.internal.js b/packages/react-reconciler/src/__tests__/ReactSuspenseWithNoopRenderer-test.internal.js index d27b422f46c58..646ebd28afe8c 100644 --- a/packages/react-reconciler/src/__tests__/ReactSuspenseWithNoopRenderer-test.internal.js +++ b/packages/react-reconciler/src/__tests__/ReactSuspenseWithNoopRenderer-test.internal.js @@ -3758,4 +3758,119 @@ describe('ReactSuspenseWithNoopRenderer', () => { , ); }); + + // Regression: https://github.com/facebook/react/issues/18486 + it.experimental( + 'does not get stuck in pending state with render phase updates', + async () => { + let setTextWithTransition; + + function App() { + const [startTransition, isPending] = React.useTransition({ + timeoutMs: 30000, + }); + const [text, setText] = React.useState(''); + const [mirror, setMirror] = React.useState(''); + + if (text !== mirror) { + // Render phase update was needed to repro the bug. + setMirror(text); + } + + setTextWithTransition = value => { + startTransition(() => { + setText(value); + }); + }; + + return ( + <> + {isPending ? : null} + {text !== '' ? : } + + ); + } + + function Root() { + return ( + }> + + + ); + } + + const root = ReactNoop.createRoot(); + await ReactNoop.act(async () => { + root.render(); + }); + expect(Scheduler).toHaveYielded(['']); + expect(root).toMatchRenderedOutput(); + + // Update to "a". That will suspend. + await ReactNoop.act(async () => { + setTextWithTransition('a'); + // Let it expire. This is important for the repro. + Scheduler.unstable_advanceTime(1000); + expect(Scheduler).toFlushAndYield([ + 'Pending...', + '', + 'Suspend! [a]', + 'Loading...', + ]); + }); + expect(Scheduler).toHaveYielded([]); + expect(root).toMatchRenderedOutput( + <> + + + , + ); + + // Update to "b". That will suspend, too. + await ReactNoop.act(async () => { + setTextWithTransition('b'); + expect(Scheduler).toFlushAndYield([ + // Neither is resolved yet. + 'Pending...', + 'Suspend! [a]', + 'Loading...', + 'Suspend! [b]', + 'Loading...', + ]); + }); + expect(Scheduler).toHaveYielded([]); + expect(root).toMatchRenderedOutput( + <> + + + , + ); + + // Resolve "a". But "b" is still pending. + await ReactNoop.act(async () => { + await resolveText('a'); + }); + expect(Scheduler).toHaveYielded([ + 'Promise resolved [a]', + 'Pending...', + 'a', + 'Suspend! [b]', + 'Loading...', + ]); + expect(root).toMatchRenderedOutput( + <> + + + , + ); + + // Resolve "b". This should remove the pending state. + await ReactNoop.act(async () => { + await resolveText('b'); + }); + expect(Scheduler).toHaveYielded(['Promise resolved [b]', 'b']); + // The bug was that the pending state got stuck forever. + expect(root).toMatchRenderedOutput(); + }, + ); });