diff --git a/packages/react-dom/src/__tests__/ReactCompositeComponent-test.js b/packages/react-dom/src/__tests__/ReactCompositeComponent-test.js index 908d418cac7e..63cd1f97c531 100644 --- a/packages/react-dom/src/__tests__/ReactCompositeComponent-test.js +++ b/packages/react-dom/src/__tests__/ReactCompositeComponent-test.js @@ -493,43 +493,6 @@ describe('ReactCompositeComponent', () => { ReactDOM.render(, container); }); - it('should warn about `setState` in getChildContext', () => { - const container = document.createElement('div'); - - let renderPasses = 0; - - class Component extends React.Component { - state = {value: 0}; - - getChildContext() { - if (this.state.value === 0) { - this.setState({value: 1}); - } - } - - render() { - renderPasses++; - return
; - } - } - Component.childContextTypes = {}; - - let instance; - - expect(() => { - instance = ReactDOM.render(, container); - }).toErrorDev( - 'Warning: setState(...): Cannot call setState() inside getChildContext()', - ); - - expect(renderPasses).toBe(2); - expect(instance.state.value).toBe(1); - - // Test deduplication; (no additional warnings are expected). - ReactDOM.unmountComponentAtNode(container); - ReactDOM.render(, container); - }); - it('should cleanup even if render() fatals', () => { class BadComponent extends React.Component { render() { @@ -1786,4 +1749,114 @@ describe('ReactCompositeComponent', () => { ReactDOM.render(, container); expect(container.firstChild.tagName).toBe('DIV'); }); + + it('should not warn on updating function component from componentWillMount', () => { + let _setState; + function A() { + _setState = React.useState()[1]; + return null; + } + class B extends React.Component { + UNSAFE_componentWillMount() { + _setState({}); + } + render() { + return null; + } + } + function Parent() { + return ( + + ); + } + const container = document.createElement('div'); + ReactDOM.render(, container); + }); + + it('should not warn on updating function component from componentWillUpdate', () => { + let _setState; + function A() { + _setState = React.useState()[1]; + return null; + } + class B extends React.Component { + UNSAFE_componentWillUpdate() { + _setState({}); + } + render() { + return null; + } + } + function Parent() { + return ( + + ); + } + const container = document.createElement('div'); + ReactDOM.render(, container); + ReactDOM.render(, container); + }); + + it('should not warn on updating function component from componentWillReceiveProps', () => { + let _setState; + function A() { + _setState = React.useState()[1]; + return null; + } + class B extends React.Component { + UNSAFE_componentWillReceiveProps() { + _setState({}); + } + render() { + return null; + } + } + function Parent() { + return ( + + ); + } + const container = document.createElement('div'); + ReactDOM.render(, container); + ReactDOM.render(, container); + }); + + it('should warn on updating function component from render', () => { + let _setState; + function A() { + _setState = React.useState()[1]; + return null; + } + class B extends React.Component { + render() { + _setState({}); + return null; + } + } + function Parent() { + return ( + + ); + } + const container = document.createElement('div'); + expect(() => { + ReactDOM.render(, container); + }).toErrorDev( + 'Cannot update a component (`A`) while rendering a different component (`B`)', + ); + // Dedupe. + ReactDOM.render(, container); + }); }); diff --git a/packages/react-dom/src/__tests__/ReactDOMComponent-test.js b/packages/react-dom/src/__tests__/ReactDOMComponent-test.js index 7a0e28639952..1b9e64484889 100644 --- a/packages/react-dom/src/__tests__/ReactDOMComponent-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMComponent-test.js @@ -1242,53 +1242,6 @@ describe('ReactDOMComponent', () => { ); }); - it('should emit a warning once for a named custom component using shady DOM', () => { - const defaultCreateElement = document.createElement.bind(document); - - try { - document.createElement = element => { - const container = defaultCreateElement(element); - container.shadyRoot = {}; - return container; - }; - class ShadyComponent extends React.Component { - render() { - return ; - } - } - const node = document.createElement('div'); - expect(() => ReactDOM.render(, node)).toErrorDev( - 'ShadyComponent is using shady DOM. Using shady DOM with React can ' + - 'cause things to break subtly.', - ); - mountComponent({is: 'custom-shady-div2'}); - } finally { - document.createElement = defaultCreateElement; - } - }); - - it('should emit a warning once for an unnamed custom component using shady DOM', () => { - const defaultCreateElement = document.createElement.bind(document); - - try { - document.createElement = element => { - const container = defaultCreateElement(element); - container.shadyRoot = {}; - return container; - }; - - expect(() => mountComponent({is: 'custom-shady-div'})).toErrorDev( - 'A component is using shady DOM. Using shady DOM with React can ' + - 'cause things to break subtly.', - ); - - // No additional warnings are expected - mountComponent({is: 'custom-shady-div2'}); - } finally { - document.createElement = defaultCreateElement; - } - }); - it('should treat menuitem as a void element but still create the closing tag', () => { // menuitem is not implemented in jsdom, so this triggers the unknown warning error const container = document.createElement('div'); diff --git a/packages/react-dom/src/client/ReactDOMComponent.js b/packages/react-dom/src/client/ReactDOMComponent.js index 854137dec6fa..cdf953207d0d 100644 --- a/packages/react-dom/src/client/ReactDOMComponent.js +++ b/packages/react-dom/src/client/ReactDOMComponent.js @@ -7,8 +7,6 @@ * @flow */ -// TODO: direct imports like some-package/src/* are bad. Fix me. -import {getCurrentFiberOwnerNameInDevOrNull} from 'react-reconciler/src/ReactCurrentFiber'; import {registrationNameModules} from 'legacy-events/EventPluginRegistry'; import {canUseDOM} from 'shared/ExecutionEnvironment'; import endsWith from 'shared/endsWith'; @@ -90,7 +88,6 @@ import { import {legacyListenToEvent} from '../events/DOMLegacyEventPluginSystem'; let didWarnInvalidHydration = false; -let didWarnShadyDOM = false; let didWarnScriptTags = false; const DANGEROUSLY_SET_INNER_HTML = 'dangerouslySetInnerHTML'; @@ -509,18 +506,6 @@ export function setInitialProperties( const isCustomComponentTag = isCustomComponent(tag, rawProps); if (__DEV__) { validatePropertiesInDevelopment(tag, rawProps); - if ( - isCustomComponentTag && - !didWarnShadyDOM && - (domElement: any).shadyRoot - ) { - console.error( - '%s is using shady DOM. Using shady DOM with React can ' + - 'cause things to break subtly.', - getCurrentFiberOwnerNameInDevOrNull() || 'A component', - ); - didWarnShadyDOM = true; - } } // TODO: Make sure that we check isMounted before firing any of these events. @@ -906,18 +891,6 @@ export function diffHydratedProperties( suppressHydrationWarning = rawProps[SUPPRESS_HYDRATION_WARNING] === true; isCustomComponentTag = isCustomComponent(tag, rawProps); validatePropertiesInDevelopment(tag, rawProps); - if ( - isCustomComponentTag && - !didWarnShadyDOM && - (domElement: any).shadyRoot - ) { - console.error( - '%s is using shady DOM. Using shady DOM with React can ' + - 'cause things to break subtly.', - getCurrentFiberOwnerNameInDevOrNull() || 'A component', - ); - didWarnShadyDOM = true; - } } // TODO: Make sure that we check isMounted before firing any of these events. diff --git a/packages/react-noop-renderer/src/createReactNoop.js b/packages/react-noop-renderer/src/createReactNoop.js index 9987343b90f3..7233f7976409 100644 --- a/packages/react-noop-renderer/src/createReactNoop.js +++ b/packages/react-noop-renderer/src/createReactNoop.js @@ -785,6 +785,32 @@ function createReactNoop(reconciler: Function, useMutation: boolean) { }; }, + createLegacyRoot() { + const container = { + rootID: '' + idCounter++, + pendingChildren: [], + children: [], + }; + const fiberRoot = NoopRenderer.createContainer( + container, + LegacyRoot, + false, + null, + ); + return { + _Scheduler: Scheduler, + render(children: ReactNodeList) { + NoopRenderer.updateContainer(children, fiberRoot, null, null); + }, + getChildren() { + return getChildren(container); + }, + getChildrenAsJSX() { + return getChildrenAsJSX(container); + }, + }; + }, + getChildrenAsJSX(rootID: string = DEFAULT_ROOT_ID) { const container = rootContainers.get(rootID); return getChildrenAsJSX(container); diff --git a/packages/react-reconciler/src/ReactCurrentFiber.js b/packages/react-reconciler/src/ReactCurrentFiber.js index 47baa167eb3d..d6096791b674 100644 --- a/packages/react-reconciler/src/ReactCurrentFiber.js +++ b/packages/react-reconciler/src/ReactCurrentFiber.js @@ -23,8 +23,6 @@ import getComponentName from 'shared/getComponentName'; const ReactDebugCurrentFrame = ReactSharedInternals.ReactDebugCurrentFrame; -type LifeCyclePhase = 'render' | 'getChildContext'; - function describeFiber(fiber: Fiber): string { switch (fiber.tag) { case HostRoot: @@ -57,7 +55,7 @@ export function getStackByFiberInDevAndProd(workInProgress: Fiber): string { } export let current: Fiber | null = null; -export let phase: LifeCyclePhase | null = null; +export let isRendering: boolean = false; export function getCurrentFiberOwnerNameInDevOrNull(): string | null { if (__DEV__) { @@ -88,7 +86,7 @@ export function resetCurrentFiber() { if (__DEV__) { ReactDebugCurrentFrame.getCurrentStack = null; current = null; - phase = null; + isRendering = false; } } @@ -96,12 +94,12 @@ export function setCurrentFiber(fiber: Fiber) { if (__DEV__) { ReactDebugCurrentFrame.getCurrentStack = getCurrentFiberStackInDev; current = fiber; - phase = null; + isRendering = false; } } -export function setCurrentPhase(lifeCyclePhase: LifeCyclePhase | null) { +export function setIsRendering(rendering: boolean) { if (__DEV__) { - phase = lifeCyclePhase; + isRendering = rendering; } } diff --git a/packages/react-reconciler/src/ReactFiberBeginWork.js b/packages/react-reconciler/src/ReactFiberBeginWork.js index 8eb9f9ffb950..6d3c2abd2280 100644 --- a/packages/react-reconciler/src/ReactFiberBeginWork.js +++ b/packages/react-reconciler/src/ReactFiberBeginWork.js @@ -74,9 +74,9 @@ import ReactStrictModeWarnings from './ReactStrictModeWarnings'; import {refineResolvedLazyComponent} from 'shared/ReactLazyComponent'; import {REACT_LAZY_TYPE, getIteratorFn} from 'shared/ReactSymbols'; import { - setCurrentPhase, getCurrentFiberOwnerNameInDevOrNull, getCurrentFiberStackInDev, + setIsRendering, } from './ReactCurrentFiber'; import {startWorkTimer, cancelWorkTimer} from './ReactDebugFiberPerf'; import { @@ -193,7 +193,6 @@ let didWarnAboutContextTypeOnFunctionComponent; let didWarnAboutGetDerivedStateOnFunctionComponent; let didWarnAboutFunctionRefs; export let didWarnAboutReassigningProps; -let didWarnAboutMaxDuration; let didWarnAboutRevealOrder; let didWarnAboutTailOptions; let didWarnAboutDefaultPropsOnFunctionComponent; @@ -205,7 +204,6 @@ if (__DEV__) { didWarnAboutGetDerivedStateOnFunctionComponent = {}; didWarnAboutFunctionRefs = {}; didWarnAboutReassigningProps = false; - didWarnAboutMaxDuration = false; didWarnAboutRevealOrder = {}; didWarnAboutTailOptions = {}; didWarnAboutDefaultPropsOnFunctionComponent = {}; @@ -312,7 +310,7 @@ function updateForwardRef( prepareToReadContext(workInProgress, renderExpirationTime); if (__DEV__) { ReactCurrentOwner.current = workInProgress; - setCurrentPhase('render'); + setIsRendering(true); nextChildren = renderWithHooks( current, workInProgress, @@ -337,7 +335,7 @@ function updateForwardRef( ); } } - setCurrentPhase(null); + setIsRendering(false); } else { nextChildren = renderWithHooks( current, @@ -644,7 +642,7 @@ function updateFunctionComponent( prepareToReadContext(workInProgress, renderExpirationTime); if (__DEV__) { ReactCurrentOwner.current = workInProgress; - setCurrentPhase('render'); + setIsRendering(true); nextChildren = renderWithHooks( current, workInProgress, @@ -669,7 +667,7 @@ function updateFunctionComponent( ); } } - setCurrentPhase(null); + setIsRendering(false); } else { nextChildren = renderWithHooks( current, @@ -720,7 +718,7 @@ function updateBlock( prepareToReadContext(workInProgress, renderExpirationTime); if (__DEV__) { ReactCurrentOwner.current = workInProgress; - setCurrentPhase('render'); + setIsRendering(true); nextChildren = renderWithHooks( current, workInProgress, @@ -745,7 +743,7 @@ function updateBlock( ); } } - setCurrentPhase(null); + setIsRendering(false); } else { nextChildren = renderWithHooks( current, @@ -923,7 +921,7 @@ function finishClassComponent( } } else { if (__DEV__) { - setCurrentPhase('render'); + setIsRendering(true); nextChildren = instance.render(); if ( debugRenderPhaseSideEffectsForStrictMode && @@ -931,7 +929,7 @@ function finishClassComponent( ) { instance.render(); } - setCurrentPhase(null); + setIsRendering(false); } else { nextChildren = instance.render(); } @@ -1358,6 +1356,7 @@ function mountIndeterminateComponent( ReactStrictModeWarnings.recordLegacyContextWarning(workInProgress, null); } + setIsRendering(true); ReactCurrentOwner.current = workInProgress; value = renderWithHooks( null, @@ -1367,6 +1366,7 @@ function mountIndeterminateComponent( context, renderExpirationTime, ); + setIsRendering(false); } else { value = renderWithHooks( null, @@ -1637,18 +1637,6 @@ function updateSuspenseComponent( pushSuspenseContext(workInProgress, suspenseContext); - if (__DEV__) { - if ('maxDuration' in nextProps) { - if (!didWarnAboutMaxDuration) { - didWarnAboutMaxDuration = true; - console.error( - 'maxDuration has been removed from React. ' + - 'Remove the maxDuration prop.', - ); - } - } - } - // This next part is a bit confusing. If the children timeout, we switch to // showing the fallback children in place of the "primary" children. // However, we don't want to delete the primary children because then their @@ -2732,9 +2720,9 @@ function updateContextConsumer( let newChildren; if (__DEV__) { ReactCurrentOwner.current = workInProgress; - setCurrentPhase('render'); + setIsRendering(true); newChildren = render(newValue); - setCurrentPhase(null); + setIsRendering(false); } else { newChildren = render(newValue); } diff --git a/packages/react-reconciler/src/ReactFiberContext.js b/packages/react-reconciler/src/ReactFiberContext.js index 946d33606c81..5f4375b4c5c1 100644 --- a/packages/react-reconciler/src/ReactFiberContext.js +++ b/packages/react-reconciler/src/ReactFiberContext.js @@ -17,7 +17,7 @@ import getComponentName from 'shared/getComponentName'; import invariant from 'shared/invariant'; import checkPropTypes from 'prop-types/checkPropTypes'; -import {setCurrentPhase, getCurrentFiberStackInDev} from './ReactCurrentFiber'; +import {getCurrentFiberStackInDev} from './ReactCurrentFiber'; import {startPhaseTimer, stopPhaseTimer} from './ReactDebugFiberPerf'; import {createCursor, push, pop} from './ReactFiberStack'; @@ -210,15 +210,9 @@ function processChildContext( } let childContext; - if (__DEV__) { - setCurrentPhase('getChildContext'); - } startPhaseTimer(fiber, 'getChildContext'); childContext = instance.getChildContext(); stopPhaseTimer(); - if (__DEV__) { - setCurrentPhase(null); - } for (let contextKey in childContext) { invariant( contextKey in childContextTypes, diff --git a/packages/react-reconciler/src/ReactFiberReconciler.js b/packages/react-reconciler/src/ReactFiberReconciler.js index 8102f66c25f6..816a3946562e 100644 --- a/packages/react-reconciler/src/ReactFiberReconciler.js +++ b/packages/react-reconciler/src/ReactFiberReconciler.js @@ -70,7 +70,7 @@ import { import {createUpdate, enqueueUpdate} from './ReactUpdateQueue'; import { getStackByFiberInDevAndProd, - phase as ReactCurrentFiberPhase, + isRendering as ReactCurrentFiberIsRendering, current as ReactCurrentFiberCurrent, } from './ReactCurrentFiber'; import {StrictMode} from './ReactTypeOfMode'; @@ -259,7 +259,7 @@ export function updateContainer( if (__DEV__) { if ( - ReactCurrentFiberPhase === 'render' && + ReactCurrentFiberIsRendering && ReactCurrentFiberCurrent !== null && !didWarnAboutNestedUpdates ) { diff --git a/packages/react-reconciler/src/ReactFiberThrow.js b/packages/react-reconciler/src/ReactFiberThrow.js index 94ef5ba8225b..df2154db9d5c 100644 --- a/packages/react-reconciler/src/ReactFiberThrow.js +++ b/packages/react-reconciler/src/ReactFiberThrow.js @@ -199,9 +199,11 @@ function throwException( // to render it. let currentSource = sourceFiber.alternate; if (currentSource) { + sourceFiber.updateQueue = currentSource.updateQueue; sourceFiber.memoizedState = currentSource.memoizedState; sourceFiber.expirationTime = currentSource.expirationTime; } else { + sourceFiber.updateQueue = null; sourceFiber.memoizedState = null; } } diff --git a/packages/react-reconciler/src/ReactFiberWorkLoop.js b/packages/react-reconciler/src/ReactFiberWorkLoop.js index 3c2427594a79..0ffabf3c818c 100644 --- a/packages/react-reconciler/src/ReactFiberWorkLoop.js +++ b/packages/react-reconciler/src/ReactFiberWorkLoop.js @@ -156,7 +156,7 @@ import { import getComponentName from 'shared/getComponentName'; import ReactStrictModeWarnings from './ReactStrictModeWarnings'; import { - phase as ReactCurrentDebugFiberPhaseInDEV, + isRendering as ReactCurrentDebugFiberIsRenderingInDEV, resetCurrentFiber as resetCurrentDebugFiberInDEV, setCurrentFiber as setCurrentDebugFiberInDEV, getStackByFiberInDevAndProd, @@ -2793,42 +2793,49 @@ if (__DEV__ && replayFailedUnitOfWorkWithInvokeGuardedCallback) { } let didWarnAboutUpdateInRender = false; -let didWarnAboutUpdateInGetChildContext = false; +let didWarnAboutUpdateInRenderForAnotherComponent; +if (__DEV__) { + didWarnAboutUpdateInRenderForAnotherComponent = new Set(); +} + function warnAboutRenderPhaseUpdatesInDEV(fiber) { if (__DEV__) { - if ((executionContext & RenderContext) !== NoContext) { + if ( + ReactCurrentDebugFiberIsRenderingInDEV && + (executionContext & RenderContext) !== NoContext + ) { switch (fiber.tag) { case FunctionComponent: case ForwardRef: case SimpleMemoComponent: { - console.error( - 'Cannot update a component from inside the function body of a ' + - 'different component.', - ); + const renderingComponentName = + (workInProgress && getComponentName(workInProgress.type)) || + 'Unknown'; + // Dedupe by the rendering component because it's the one that needs to be fixed. + const dedupeKey = renderingComponentName; + if (!didWarnAboutUpdateInRenderForAnotherComponent.has(dedupeKey)) { + didWarnAboutUpdateInRenderForAnotherComponent.add(dedupeKey); + const setStateComponentName = + getComponentName(fiber.type) || 'Unknown'; + console.error( + 'Cannot update a component (`%s`) while rendering a ' + + 'different component (`%s`). To locate the bad setState() call inside `%s`, ' + + 'follow the stack trace as described in https://fb.me/setstate-in-render', + setStateComponentName, + renderingComponentName, + renderingComponentName, + ); + } break; } case ClassComponent: { - switch (ReactCurrentDebugFiberPhaseInDEV) { - case 'getChildContext': - if (didWarnAboutUpdateInGetChildContext) { - return; - } - console.error( - 'setState(...): Cannot call setState() inside getChildContext()', - ); - didWarnAboutUpdateInGetChildContext = true; - break; - case 'render': - if (didWarnAboutUpdateInRender) { - return; - } - console.error( - 'Cannot update during an existing state transition (such as ' + - 'within `render`). Render methods should be a pure ' + - 'function of props and state.', - ); - didWarnAboutUpdateInRender = true; - break; + if (!didWarnAboutUpdateInRender) { + console.error( + 'Cannot update during an existing state transition (such as ' + + 'within `render`). Render methods should be a pure ' + + 'function of props and state.', + ); + didWarnAboutUpdateInRender = true; } break; } diff --git a/packages/react-reconciler/src/__tests__/ReactHooks-test.internal.js b/packages/react-reconciler/src/__tests__/ReactHooks-test.internal.js index f9073f1bf701..c1d0ea18f3f2 100644 --- a/packages/react-reconciler/src/__tests__/ReactHooks-test.internal.js +++ b/packages/react-reconciler/src/__tests__/ReactHooks-test.internal.js @@ -1087,7 +1087,7 @@ describe('ReactHooks', () => { ), ).toErrorDev([ 'Context can only be read while React is rendering', - 'Cannot update a component from inside the function body of a different component.', + 'Cannot update a component (`Fn`) while rendering a different component (`Cls`).', ]); }); @@ -1783,8 +1783,8 @@ describe('ReactHooks', () => { if (__DEV__) { expect(console.error).toHaveBeenCalledTimes(2); expect(console.error.calls.argsFor(0)[0]).toContain( - 'Warning: Cannot update a component from inside the function body ' + - 'of a different component.%s', + 'Warning: Cannot update a component (`%s`) while rendering ' + + 'a different component (`%s`).', ); } }); diff --git a/packages/react-reconciler/src/__tests__/ReactHooksWithNoopRenderer-test.internal.js b/packages/react-reconciler/src/__tests__/ReactHooksWithNoopRenderer-test.internal.js index 81b774bd0a55..bb14e5f7b1c9 100644 --- a/packages/react-reconciler/src/__tests__/ReactHooksWithNoopRenderer-test.internal.js +++ b/packages/react-reconciler/src/__tests__/ReactHooksWithNoopRenderer-test.internal.js @@ -430,7 +430,7 @@ function loadModules({ function Bar({triggerUpdate}) { if (triggerUpdate) { - setStep(1); + setStep(x => x + 1); } return ; } @@ -458,10 +458,21 @@ function loadModules({ expect(() => expect(Scheduler).toFlushAndYield(['Foo [0]', 'Bar', 'Foo [1]']), ).toErrorDev([ - 'Cannot update a component from inside the function body of a ' + - 'different component.', + 'Cannot update a component (`Foo`) while rendering a ' + + 'different component (`Bar`). To locate the bad setState() call inside `Bar`', ]); }); + + // It should not warn again (deduplication). + await ReactNoop.act(async () => { + root.render( + <> + + + , + ); + expect(Scheduler).toFlushAndYield(['Foo [1]', 'Bar', 'Foo [2]']); + }); }); it('keeps restarting until there are no more new updates', () => { diff --git a/packages/react-reconciler/src/__tests__/ReactSuspenseWithNoopRenderer-test.internal.js b/packages/react-reconciler/src/__tests__/ReactSuspenseWithNoopRenderer-test.internal.js index 0cca771b1df2..530544d6cada 100644 --- a/packages/react-reconciler/src/__tests__/ReactSuspenseWithNoopRenderer-test.internal.js +++ b/packages/react-reconciler/src/__tests__/ReactSuspenseWithNoopRenderer-test.internal.js @@ -110,25 +110,6 @@ function loadModules({ } } - it('warns if the deprecated maxDuration option is used', () => { - function Foo() { - return ( - -
; - - ); - } - - ReactNoop.render(); - - expect(() => Scheduler.unstable_flushAll()).toErrorDev([ - 'Warning: maxDuration has been removed from React. ' + - 'Remove the maxDuration prop.' + - '\n in Suspense (at **)' + - '\n in Foo (at **)', - ]); - }); - it('does not restart rendering for initial render', async () => { function Bar(props) { Scheduler.unstable_yieldValue('Bar'); @@ -1467,6 +1448,55 @@ function loadModules({ 'Caught an error: Error in host config.', ); }); + + it('does not drop mounted effects', async () => { + let never = {then() {}}; + + let setShouldSuspend; + function App() { + const [shouldSuspend, _setShouldSuspend] = React.useState(0); + setShouldSuspend = _setShouldSuspend; + return ( + + + + ); + } + + function Child({shouldSuspend}) { + if (shouldSuspend) { + throw never; + } + + React.useEffect(() => { + Scheduler.unstable_yieldValue('Mount'); + return () => { + Scheduler.unstable_yieldValue('Unmount'); + }; + }, []); + + return 'Child'; + } + + const root = ReactNoop.createLegacyRoot(null); + await ReactNoop.act(async () => { + root.render(); + }); + expect(Scheduler).toHaveYielded(['Mount']); + expect(root).toMatchRenderedOutput('Child'); + + // Suspend the child. This puts it into an inconsistent state. + await ReactNoop.act(async () => { + setShouldSuspend(true); + }); + expect(root).toMatchRenderedOutput('Loading...'); + + // Unmount everying + await ReactNoop.act(async () => { + root.render(null); + }); + expect(Scheduler).toHaveYielded(['Unmount']); + }); }); it('does not call lifecycles of a suspended component', async () => {