diff --git a/packages/react-reconciler/src/ReactFiberWorkLoop.js b/packages/react-reconciler/src/ReactFiberWorkLoop.js index a059f50ccaa9..83d7dfedc611 100644 --- a/packages/react-reconciler/src/ReactFiberWorkLoop.js +++ b/packages/react-reconciler/src/ReactFiberWorkLoop.js @@ -2919,6 +2919,11 @@ if (__DEV__ && replayFailedUnitOfWorkWithInvokeGuardedCallback) { } let didWarnAboutUpdateInRender = false; +let didWarnAboutUpdateInRenderForAnotherComponent; +if (__DEV__) { + didWarnAboutUpdateInRenderForAnotherComponent = new Set(); +} + function warnAboutRenderPhaseUpdatesInDEV(fiber) { if (__DEV__) { if ((executionContext & RenderContext) !== NoContext) { @@ -2926,10 +2931,24 @@ function warnAboutRenderPhaseUpdatesInDEV(fiber) { 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'; + const setStateComponentName = + getComponentName(fiber.type) || 'Unknown'; + const dedupeKey = + renderingComponentName + ' ' + setStateComponentName; + if (!didWarnAboutUpdateInRenderForAnotherComponent.has(dedupeKey)) { + didWarnAboutUpdateInRenderForAnotherComponent.add(dedupeKey); + console.error( + 'Cannot update a component (`%s`) from inside the function body of 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: { diff --git a/packages/react-reconciler/src/__tests__/ReactHooks-test.internal.js b/packages/react-reconciler/src/__tests__/ReactHooks-test.internal.js index f9073f1bf701..6de3567163a3 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`) from inside the function body of 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`) from inside the function body ' + + 'of 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 9bb243802656..302894091b86 100644 --- a/packages/react-reconciler/src/__tests__/ReactHooksWithNoopRenderer-test.internal.js +++ b/packages/react-reconciler/src/__tests__/ReactHooksWithNoopRenderer-test.internal.js @@ -412,7 +412,7 @@ describe('ReactHooksWithNoopRenderer', () => { function Bar({triggerUpdate}) { if (triggerUpdate) { - setStep(1); + setStep(x => x + 1); } return ; } @@ -440,10 +440,21 @@ describe('ReactHooksWithNoopRenderer', () => { 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`) from inside the function body of 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', () => {