@samitouri / QOS-React-2 / commits / 95df39b6b1

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