diff --git a/packages/react-reconciler/src/ReactFiberWorkLoop.js b/packages/react-reconciler/src/ReactFiberWorkLoop.js index 0ff8ebe047699..488a911cdc020 100644 --- a/packages/react-reconciler/src/ReactFiberWorkLoop.js +++ b/packages/react-reconciler/src/ReactFiberWorkLoop.js @@ -372,7 +372,7 @@ export function scheduleUpdateOnFiber( expirationTime: ExpirationTime, ) { checkForNestedUpdates(); - warnAboutInvalidUpdatesOnClassComponentsInDEV(fiber); + warnAboutRenderPhaseUpdatesInDEV(fiber); const root = markUpdateTimeFromFiberToRoot(fiber, expirationTime); if (root === null) { @@ -2663,32 +2663,45 @@ if (__DEV__ && replayFailedUnitOfWorkWithInvokeGuardedCallback) { let didWarnAboutUpdateInRender = false; let didWarnAboutUpdateInGetChildContext = false; -function warnAboutInvalidUpdatesOnClassComponentsInDEV(fiber) { - if (__DEV__) { - if (fiber.tag === ClassComponent) { - switch (ReactCurrentDebugFiberPhaseInDEV) { - case 'getChildContext': - if (didWarnAboutUpdateInGetChildContext) { - return; - } - warningWithoutStack( - false, - 'setState(...): Cannot call setState() inside getChildContext()', - ); - didWarnAboutUpdateInGetChildContext = true; - break; - case 'render': - if (didWarnAboutUpdateInRender) { - return; - } - warningWithoutStack( - false, - '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; +function warnAboutRenderPhaseUpdatesInDEV(fiber) { + if (__DEV__ && (executionContext & RenderContext) !== NoContext) { + switch (fiber.tag) { + case FunctionComponent: + case ForwardRef: + case SimpleMemoComponent: { + warning( + false, + 'Cannot update a component from inside the function body of a ' + + 'different component.', + ); + break; + } + case ClassComponent: { + switch (ReactCurrentDebugFiberPhaseInDEV) { + case 'getChildContext': + if (didWarnAboutUpdateInGetChildContext) { + return; + } + warningWithoutStack( + false, + 'setState(...): Cannot call setState() inside getChildContext()', + ); + didWarnAboutUpdateInGetChildContext = true; + break; + case 'render': + if (didWarnAboutUpdateInRender) { + return; + } + warningWithoutStack( + false, + '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; + } + break; } } } diff --git a/packages/react-reconciler/src/__tests__/ReactHooksWithNoopRenderer-test.internal.js b/packages/react-reconciler/src/__tests__/ReactHooksWithNoopRenderer-test.internal.js index 151688592507f..4ec0c077f12bc 100644 --- a/packages/react-reconciler/src/__tests__/ReactHooksWithNoopRenderer-test.internal.js +++ b/packages/react-reconciler/src/__tests__/ReactHooksWithNoopRenderer-test.internal.js @@ -491,6 +491,50 @@ describe('ReactHooksWithNoopRenderer', () => { ]); expect(ReactNoop.getChildren()).toEqual([span(22)]); }); + + it('warns about render phase update on a different component', async () => { + let setStep; + function Foo() { + const [step, _setStep] = useState(0); + setStep = _setStep; + return ; + } + + function Bar({triggerUpdate}) { + if (triggerUpdate) { + setStep(1); + } + return ; + } + + const root = ReactNoop.createRoot(); + + await ReactNoop.act(async () => { + root.render( + <> + + + , + ); + }); + expect(Scheduler).toHaveYielded(['Foo [0]', 'Bar']); + + // Bar will update Foo during its render phase. React should warn. + await ReactNoop.act(async () => { + root.render( + <> + + + , + ); + expect(() => + expect(Scheduler).toFlushAndYield(['Foo [0]', 'Bar', 'Foo [1]']), + ).toWarnDev([ + 'Cannot update a component from inside the function body of a ' + + 'different component.', + ]); + }); + }); }); describe('useReducer', () => {