improve error message for cross-functional component updates (#18316)
* improve error message for cross-functional component updates * correctly use %s by quoting it * use workInProgress and lint * add test assertion * fix test * Improve the error message Co-authored-by: Dan Abramov <dan.abramov@me.com>
Jovi De Croock committed
Mar 16, 2020 at 18:05 UTC
95df39b6b1703c1ca13e79f65de747a2b434e9ab
3 files changed
+40
-10
packages/react-reconciler/src/ReactFiberWorkLoop.js
+23
-4
@@ -2919,6 +2919,11 @@ if (__DEV__ && replayFailedUnitOfWorkWithInvokeGuardedCallback) {
2919
}
2920
2921
let didWarnAboutUpdateInRender = false;
2922
+let didWarnAboutUpdateInRenderForAnotherComponent;
2923
+if (__DEV__) {
2924
+ didWarnAboutUpdateInRenderForAnotherComponent = new Set();
2925
+}
2926
+
2927
function warnAboutRenderPhaseUpdatesInDEV(fiber) {
2928
if (__DEV__) {
2929
if ((executionContext & RenderContext) !== NoContext) {
@@ -2926,10 +2931,24 @@ function warnAboutRenderPhaseUpdatesInDEV(fiber) {
2931
case FunctionComponent:
2932
case ForwardRef:
2933
case SimpleMemoComponent: {
2929
- console.error(
2930
- 'Cannot update a component from inside the function body of a ' +
2931
- 'different component.',
2932
- );
2934
+ const renderingComponentName =
2935
+ (workInProgress && getComponentName(workInProgress.type)) ||
2936
+ 'Unknown';
2937
+ const setStateComponentName =
2938
+ getComponentName(fiber.type) || 'Unknown';
2939
+ const dedupeKey =
2940
+ renderingComponentName + ' ' + setStateComponentName;
2941
+ if (!didWarnAboutUpdateInRenderForAnotherComponent.has(dedupeKey)) {
2942
+ didWarnAboutUpdateInRenderForAnotherComponent.add(dedupeKey);
2943
+ console.error(
2944
+ 'Cannot update a component (`%s`) from inside the function body of a ' +
2945
+ 'different component (`%s`). To locate the bad setState() call inside `%s`, ' +
2946
+ 'follow the stack trace as described in https://fb.me/setstate-in-render',
2947
+ setStateComponentName,
2948
+ renderingComponentName,
2949
+ renderingComponentName,
2950
+ );
2951
+ }
2952
break;
2953
}
2954
case ClassComponent: {
packages/react-reconciler/src/__tests__/ReactHooks-test.internal.js
+3
-3
@@ -1087,7 +1087,7 @@ describe('ReactHooks', () => {
1087
),
1088
).toErrorDev([
1089
'Context can only be read while React is rendering',
1090
- 'Cannot update a component from inside the function body of a different component.',
1090
+ 'Cannot update a component (`Fn`) from inside the function body of a different component (`Cls`).',
1091
]);
1092
});
1093
@@ -1783,8 +1783,8 @@ describe('ReactHooks', () => {
1783
if (__DEV__) {
1784
expect(console.error).toHaveBeenCalledTimes(2);
1785
expect(console.error.calls.argsFor(0)[0]).toContain(
1786
- 'Warning: Cannot update a component from inside the function body ' +
1787
- 'of a different component.%s',
1786
+ 'Warning: Cannot update a component (`%s`) from inside the function body ' +
1787
+ 'of a different component (`%s`).',
1788
);
1789
}
1790
});
packages/react-reconciler/src/__tests__/ReactHooksWithNoopRenderer-test.internal.js
+14
-3
@@ -412,7 +412,7 @@ describe('ReactHooksWithNoopRenderer', () => {
412
413
function Bar({triggerUpdate}) {
414
if (triggerUpdate) {
415
- setStep(1);
415
+ setStep(x => x + 1);
416
}
417
return <Text text="Bar" />;
418
}
@@ -440,10 +440,21 @@ describe('ReactHooksWithNoopRenderer', () => {
440
expect(() =>
441
expect(Scheduler).toFlushAndYield(['Foo [0]', 'Bar', 'Foo [1]']),
442
).toErrorDev([
443
- 'Cannot update a component from inside the function body of a ' +
444
- 'different component.',
443
+ 'Cannot update a component (`Foo`) from inside the function body of a ' +
444
+ 'different component (`Bar`). To locate the bad setState() call inside `Bar`',
445
]);
446
});
447
+
448
+ // It should not warn again (deduplication).
449
+ await ReactNoop.act(async () => {
450
+ root.render(
451
+ <>
452
+ <Foo />
453
+ <Bar triggerUpdate={true} />
454
+ </>,
455
+ );
456
+ expect(Scheduler).toFlushAndYield(['Foo [1]', 'Bar', 'Foo [2]']);
457
+ });
458
});
459
460
it('keeps restarting until there are no more new updates', () => {