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', () => {