diff --git a/packages/react-reconciler/src/__tests__/StrictEffectsMode-test.js b/packages/react-reconciler/src/__tests__/StrictEffectsMode-test.js index c1354c47e5a30..f673bf01c2cc0 100644 --- a/packages/react-reconciler/src/__tests__/StrictEffectsMode-test.js +++ b/packages/react-reconciler/src/__tests__/StrictEffectsMode-test.js @@ -772,4 +772,177 @@ describe('StrictEffectsMode', () => { 'useEffect unmount', ]); }); + + // @gate __DEV__ + it('should double invoke effects after a re-suspend', async () => { + // Not using Scheduler.log because it silences double render logs. + let log = []; + let shouldSuspend = true; + let resolve; + const suspensePromise = new Promise(_resolve => { + resolve = _resolve; + }); + function Fallback() { + log.push('Fallback'); + return 'Loading'; + } + + function Parent({prop}) { + log.push('Parent rendered'); + + React.useEffect(() => { + log.push('Parent create'); + return () => { + log.push('Parent destroy'); + }; + }, []); + + React.useEffect(() => { + log.push('Parent dep create'); + return () => { + log.push('Parent dep destroy'); + }; + }, [prop]); + + return ( + }> + + + ); + } + + function Child({prop}) { + const [count, forceUpdate] = React.useState(0); + const ref = React.useRef(null); + log.push('Child rendered'); + React.useEffect(() => { + log.push('Child create'); + return () => { + log.push('Child destroy'); + ref.current = true; + }; + }, []); + const key = `${prop}-${count}`; + React.useEffect(() => { + log.push('Child dep create'); + if (ref.current === true) { + ref.current = false; + forceUpdate(c => c + 1); + log.push('-----------------------after setState'); + return; + } + + return () => { + log.push('Child dep destroy'); + }; + }, [key]); + + if (shouldSuspend) { + log.push('Child suspended'); + throw suspensePromise; + } + return null; + } + + // Initial mount + shouldSuspend = false; + await act(() => { + ReactNoop.render( + + + , + ); + }); + + // Now re-suspend + shouldSuspend = true; + log = []; + await act(() => { + ReactNoop.render( + + + , + ); + }); + + // while suspended, update + log.push('-----------------------after update'); + await act(() => { + ReactNoop.render( + + + , + ); + }); + + // Now resolve and commit + log.push('-----------------------after suspense'); + + await act(() => { + resolve(); + shouldSuspend = false; + }); + + if (gate(flags => flags.useModernStrictMode)) { + expect(log).toEqual([ + 'Parent rendered', + 'Parent rendered', + 'Child rendered', + 'Child suspended', + 'Fallback', + 'Fallback', + '-----------------------after update', + 'Parent rendered', + 'Parent rendered', + 'Child rendered', + 'Child suspended', + 'Fallback', + 'Fallback', + 'Parent dep destroy', + 'Parent dep create', + '-----------------------after suspense', + 'Child rendered', + 'Child rendered', + // !!! Committed, destroy and create effect. + // !!! The other effect is not destroyed and created + // !!! because the dep didn't change + 'Child dep destroy', + 'Child dep create', + + // Double invoke both effects + 'Child destroy', + 'Child dep destroy', + 'Child create', + 'Child dep create', + // Fires setState + '-----------------------after setState', + 'Child rendered', + 'Child rendered', + 'Child dep create', + ]); + } else { + expect(log).toEqual([ + 'Parent rendered', + 'Parent rendered', + 'Child rendered', + 'Child suspended', + 'Fallback', + 'Fallback', + '-----------------------after update', + 'Parent rendered', + 'Parent rendered', + 'Child rendered', + 'Child suspended', + 'Fallback', + 'Fallback', + 'Parent dep destroy', + 'Parent dep create', + '-----------------------after suspense', + 'Child rendered', + 'Child rendered', + 'Child dep destroy', + 'Child dep create', + ]); + } + }); });