@samitouri / QOS-React-2 / commits / 3ac551e855

Dim `console` calls on additional Effect invocations due to `StrictMode` (#29007)

Sebastian Silbermann committed May 22, 2024 at 11:39 UTC 3ac551e855f9bec3161da2fc8787958aa62113db
9 files changed +272 -30
packages/internal-test-utils/__tests__/ReactInternalTestUtils-test.js
+20 -1
@@ -146,6 +146,12 @@ describe('ReactInternalTestUtils', () => {
146 test('assertLog', async () => {
147 const Yield = ({id}) => {
148 Scheduler.log(id);
149 + React.useEffect(() => {
150 + Scheduler.log(`create effect ${id}`);
151 + return () => {
152 + Scheduler.log(`cleanup effect ${id}`);
153 + };
154 + });
155 return id;
156 };
157
@@ -167,7 +173,20 @@ describe('ReactInternalTestUtils', () => {
173 </React.StrictMode>
174 );
175 });
170 - assertLog(['A', 'B', 'C']);
176 + assertLog([
177 + 'A',
178 + 'B',
179 + 'C',
180 + 'create effect A',
181 + 'create effect B',
182 + 'create effect C',
183 + ]);
184 +
185 + await act(() => {
186 + root.render(null);
187 + });
188 +
189 + assertLog(['cleanup effect A', 'cleanup effect B', 'cleanup effect C']);
190 });
191 });
192
packages/react-devtools-shared/src/__tests__/console-test.js
+162
@@ -625,6 +625,168 @@ describe('console', () => {
625 expect(mockGroupCollapsed.mock.calls[0][0]).toBe('groupCollapsed');
626 });
627
628 + it('should double log from Effects if hideConsoleLogsInStrictMode is disabled in Strict mode', () => {
629 + global.__REACT_DEVTOOLS_APPEND_COMPONENT_STACK__ = false;
630 + global.__REACT_DEVTOOLS_HIDE_CONSOLE_LOGS_IN_STRICT_MODE__ = false;
631 +
632 + const container = document.createElement('div');
633 + const root = ReactDOMClient.createRoot(container);
634 +
635 + function App() {
636 + React.useEffect(() => {
637 + fakeConsole.log('log effect create');
638 + fakeConsole.warn('warn effect create');
639 + fakeConsole.error('error effect create');
640 + fakeConsole.info('info effect create');
641 + fakeConsole.group('group effect create');
642 + fakeConsole.groupCollapsed('groupCollapsed effect create');
643 +
644 + return () => {
645 + fakeConsole.log('log effect cleanup');
646 + fakeConsole.warn('warn effect cleanup');
647 + fakeConsole.error('error effect cleanup');
648 + fakeConsole.info('info effect cleanup');
649 + fakeConsole.group('group effect cleanup');
650 + fakeConsole.groupCollapsed('groupCollapsed effect cleanup');
651 + };
652 + });
653 +
654 + return <div />;
655 + }
656 +
657 + act(() =>
658 + root.render(
659 + <React.StrictMode>
660 + <App />
661 + </React.StrictMode>,
662 + ),
663 + );
664 + expect(mockLog.mock.calls).toEqual([
665 + ['log effect create'],
666 + [
667 + '%c%s',
668 + `color: ${process.env.DARK_MODE_DIMMED_LOG_COLOR}`,
669 + 'log effect cleanup',
670 + ],
671 + [
672 + '%c%s',
673 + `color: ${process.env.DARK_MODE_DIMMED_LOG_COLOR}`,
674 + 'log effect create',
675 + ],
676 + ]);
677 + expect(mockWarn.mock.calls).toEqual([
678 + ['warn effect create'],
679 + [
680 + '%c%s',
681 + `color: ${process.env.DARK_MODE_DIMMED_WARNING_COLOR}`,
682 + 'warn effect cleanup',
683 + ],
684 + [
685 + '%c%s',
686 + `color: ${process.env.DARK_MODE_DIMMED_WARNING_COLOR}`,
687 + 'warn effect create',
688 + ],
689 + ]);
690 + expect(mockError.mock.calls).toEqual([
691 + ['error effect create'],
692 + [
693 + '%c%s',
694 + `color: ${process.env.DARK_MODE_DIMMED_ERROR_COLOR}`,
695 + 'error effect cleanup',
696 + ],
697 + [
698 + '%c%s',
699 + `color: ${process.env.DARK_MODE_DIMMED_ERROR_COLOR}`,
700 + 'error effect create',
701 + ],
702 + ]);
703 + expect(mockInfo.mock.calls).toEqual([
704 + ['info effect create'],
705 + [
706 + '%c%s',
707 + `color: ${process.env.DARK_MODE_DIMMED_LOG_COLOR}`,
708 + 'info effect cleanup',
709 + ],
710 + [
711 + '%c%s',
712 + `color: ${process.env.DARK_MODE_DIMMED_LOG_COLOR}`,
713 + 'info effect create',
714 + ],
715 + ]);
716 + expect(mockGroup.mock.calls).toEqual([
717 + ['group effect create'],
718 + [
719 + '%c%s',
720 + `color: ${process.env.DARK_MODE_DIMMED_LOG_COLOR}`,
721 + 'group effect cleanup',
722 + ],
723 + [
724 + '%c%s',
725 + `color: ${process.env.DARK_MODE_DIMMED_LOG_COLOR}`,
726 + 'group effect create',
727 + ],
728 + ]);
729 + expect(mockGroupCollapsed.mock.calls).toEqual([
730 + ['groupCollapsed effect create'],
731 + [
732 + '%c%s',
733 + `color: ${process.env.DARK_MODE_DIMMED_LOG_COLOR}`,
734 + 'groupCollapsed effect cleanup',
735 + ],
736 + [
737 + '%c%s',
738 + `color: ${process.env.DARK_MODE_DIMMED_LOG_COLOR}`,
739 + 'groupCollapsed effect create',
740 + ],
741 + ]);
742 + });
743 +
744 + it('should not double log from Effects if hideConsoleLogsInStrictMode is enabled in Strict mode', () => {
745 + global.__REACT_DEVTOOLS_APPEND_COMPONENT_STACK__ = false;
746 + global.__REACT_DEVTOOLS_HIDE_CONSOLE_LOGS_IN_STRICT_MODE__ = true;
747 +
748 + const container = document.createElement('div');
749 + const root = ReactDOMClient.createRoot(container);
750 +
751 + function App() {
752 + React.useEffect(() => {
753 + fakeConsole.log('log effect create');
754 + fakeConsole.warn('warn effect create');
755 + fakeConsole.error('error effect create');
756 + fakeConsole.info('info effect create');
757 + fakeConsole.group('group effect create');
758 + fakeConsole.groupCollapsed('groupCollapsed effect create');
759 +
760 + return () => {
761 + fakeConsole.log('log effect cleanup');
762 + fakeConsole.warn('warn effect cleanup');
763 + fakeConsole.error('error effect cleanup');
764 + fakeConsole.info('info effect cleanup');
765 + fakeConsole.group('group effect cleanup');
766 + fakeConsole.groupCollapsed('groupCollapsed effect cleanup');
767 + };
768 + });
769 +
770 + return <div />;
771 + }
772 +
773 + act(() =>
774 + root.render(
775 + <React.StrictMode>
776 + <App />
777 + </React.StrictMode>,
778 + ),
779 + );
780 + expect(mockLog.mock.calls).toEqual([['log effect create']]);
781 + expect(mockWarn.mock.calls).toEqual([['warn effect create']]);
782 + expect(mockError.mock.calls).toEqual([['error effect create']]);
783 + expect(mockInfo.mock.calls).toEqual([['info effect create']]);
784 + expect(mockGroup.mock.calls).toEqual([['group effect create']]);
785 + expect(mockGroupCollapsed.mock.calls).toEqual([
786 + ['groupCollapsed effect create'],
787 + ]);
788 + });
789 +
790 it('should double log from useMemo if hideConsoleLogsInStrictMode is disabled in Strict mode', () => {
791 global.__REACT_DEVTOOLS_APPEND_COMPONENT_STACK__ = false;
792 global.__REACT_DEVTOOLS_HIDE_CONSOLE_LOGS_IN_STRICT_MODE__ = false;
packages/react-devtools-shared/src/backend/console.js
+2 -2
@@ -294,7 +294,7 @@ export function unpatch(): void {
294
295 let unpatchForStrictModeFn: null | (() => void) = null;
296
297 -// NOTE: KEEP IN SYNC with src/hook.js:patchConsoleForInitialRenderInStrictMode
297 +// NOTE: KEEP IN SYNC with src/hook.js:patchConsoleForInitialCommitInStrictMode
298 export function patchForStrictMode() {
299 if (consoleManagedByDevToolsDuringStrictMode) {
300 const overrideConsoleMethods = [
@@ -359,7 +359,7 @@ export function patchForStrictMode() {
359 }
360 }
361
362 -// NOTE: KEEP IN SYNC with src/hook.js:unpatchConsoleForInitialRenderInStrictMode
362 +// NOTE: KEEP IN SYNC with src/hook.js:unpatchConsoleForInitialCommitInStrictMode
363 export function unpatchForStrictMode(): void {
364 if (consoleManagedByDevToolsDuringStrictMode) {
365 if (unpatchForStrictModeFn !== null) {
packages/react-devtools-shared/src/devtools/views/Settings/DebuggingSettings.js
+8 -1
@@ -75,7 +75,14 @@ export default function DebuggingSettings(_: {}): React.Node {
75 setHideConsoleLogsInStrictMode(currentTarget.checked)
76 }
77 />{' '}
78 - Hide logs during second render in Strict Mode
78 + Hide logs during additional invocations in{' '}
79 + <a
80 + className={styles.StrictModeLink}
81 + target="_blank"
82 + rel="noopener noreferrer"
83 + href="https://react.dev/reference/react/StrictMode">
84 + Strict Mode
85 + </a>
86 </label>
87 </div>
88 </div>
packages/react-devtools-shared/src/devtools/views/Settings/SettingsShared.css
+2 -2
@@ -141,7 +141,7 @@
141 border-radius: 0.25rem;
142 }
143
144 -.ReleaseNotesLink {
144 +.ReleaseNotesLink, .StrictModeLink {
145 color: var(--color-button-active);
146 }
147
@@ -153,4 +153,4 @@
153 list-style: none;
154 padding: 0;
155 margin: 0;
156 -}
\ No newline at end of file
156 +}
packages/react-devtools-shared/src/hook.js
+5 -5
@@ -225,7 +225,7 @@ export function installHook(target: any): DevToolsHook | null {
225 // React and DevTools are connecting and the renderer interface isn't avaiable
226 // and we want to be able to have consistent logging behavior for double logs
227 // during the initial renderer.
228 - function patchConsoleForInitialRenderInStrictMode({
228 + function patchConsoleForInitialCommitInStrictMode({
229 hideConsoleLogsInStrictMode,
230 browserTheme,
231 }: {
@@ -311,7 +311,7 @@ export function installHook(target: any): DevToolsHook | null {
311 }
312
313 // NOTE: KEEP IN SYNC with src/backend/console.js:unpatchForStrictMode
314 - function unpatchConsoleForInitialRenderInStrictMode() {
314 + function unpatchConsoleForInitialCommitInStrictMode() {
315 if (unpatchFn !== null) {
316 unpatchFn();
317 unpatchFn = null;
@@ -451,19 +451,19 @@ export function installHook(target: any): DevToolsHook | null {
451 rendererInterface.unpatchConsoleForStrictMode();
452 }
453 } else {
454 - // This should only happen during initial render in the extension before DevTools
454 + // This should only happen during initial commit in the extension before DevTools
455 // finishes its handshake with the injected renderer
456 if (isStrictMode) {
457 const hideConsoleLogsInStrictMode =
458 window.__REACT_DEVTOOLS_HIDE_CONSOLE_LOGS_IN_STRICT_MODE__ === true;
459 const browserTheme = window.__REACT_DEVTOOLS_BROWSER_THEME__;
460
461 - patchConsoleForInitialRenderInStrictMode({
461 + patchConsoleForInitialCommitInStrictMode({
462 hideConsoleLogsInStrictMode,
463 browserTheme,
464 });
465 } else {
466 - unpatchConsoleForInitialRenderInStrictMode();
466 + unpatchConsoleForInitialCommitInStrictMode();
467 }
468 }
469 }
packages/react-reconciler/src/ReactFiberWorkLoop.js
+3
@@ -251,6 +251,7 @@ import {
251 markRenderStopped,
252 onCommitRoot as onCommitRootDevTools,
253 onPostCommitRoot as onPostCommitRootDevTools,
254 + setIsStrictModeForDevtools,
255 } from './ReactFiberDevToolsHook';
256 import {onCommitRoot as onCommitRootTestSelector} from './ReactTestSelectors';
257 import {releaseCache} from './ReactFiberCacheComponent';
@@ -3675,6 +3676,7 @@ function doubleInvokeEffectsOnFiber(
3676 fiber: Fiber,
3677 shouldDoubleInvokePassiveEffects: boolean = true,
3678 ) {
3679 + setIsStrictModeForDevtools(true);
3680 disappearLayoutEffects(fiber);
3681 if (shouldDoubleInvokePassiveEffects) {
3682 disconnectPassiveEffect(fiber);
@@ -3683,6 +3685,7 @@ function doubleInvokeEffectsOnFiber(
3685 if (shouldDoubleInvokePassiveEffects) {
3686 reconnectPassiveEffects(root, fiber, NoLanes, null, false);
3687 }
3688 + setIsStrictModeForDevtools(false);
3689 }
3690
3691 function doubleInvokeEffectsInDEVIfNecessary(
packages/react-reconciler/src/__tests__/StrictEffectsModeDefaults-test.internal.js
+1 -19
@@ -115,11 +115,7 @@ describe('StrictEffectsMode defaults', () => {
115 </React.StrictMode>,
116 );
117
118 - await waitForPaint([
119 - 'useLayoutEffect mount "one"',
120 - 'useLayoutEffect unmount "one"',
121 - 'useLayoutEffect mount "one"',
122 - ]);
118 + await waitForPaint(['useLayoutEffect mount "one"']);
119 expect(log).toEqual([
120 'useLayoutEffect mount "one"',
121 'useLayoutEffect unmount "one"',
@@ -142,10 +138,6 @@ describe('StrictEffectsMode defaults', () => {
138 'useLayoutEffect unmount "one"',
139 'useLayoutEffect mount "one"',
140 'useLayoutEffect mount "two"',
145 -
146 - // Since "two" is new, it should be double-invoked.
147 - 'useLayoutEffect unmount "two"',
148 - 'useLayoutEffect mount "two"',
141 ]);
142 expect(log).toEqual([
143 // Cleanup and re-run "one" (and "two") since there is no dependencies array.
@@ -196,10 +188,6 @@ describe('StrictEffectsMode defaults', () => {
188 await waitForAll([
189 'useLayoutEffect mount "one"',
190 'useEffect mount "one"',
199 - 'useLayoutEffect unmount "one"',
200 - 'useEffect unmount "one"',
201 - 'useLayoutEffect mount "one"',
202 - 'useEffect mount "one"',
191 ]);
192 expect(log).toEqual([
193 'useLayoutEffect mount "one"',
@@ -237,12 +225,6 @@ describe('StrictEffectsMode defaults', () => {
225 'useEffect unmount "one"',
226 'useEffect mount "one"',
227 'useEffect mount "two"',
240 -
241 - // Since "two" is new, it should be double-invoked.
242 - 'useLayoutEffect unmount "two"',
243 - 'useEffect unmount "two"',
244 - 'useLayoutEffect mount "two"',
245 - 'useEffect mount "two"',
228 ]);
229 expect(log).toEqual([
230 'useEffect unmount "one"',
packages/react/src/__tests__/ReactStrictMode-test.js
+69
@@ -1343,6 +1343,42 @@ describe('context legacy', () => {
1343 // and on the next render they'd get deduplicated and ignored.
1344 expect(console.log).toBeCalledWith('foo 1');
1345 });
1346 +
1347 + it('does not disable logs for effect double invoke', async () => {
1348 + let create = 0;
1349 + let cleanup = 0;
1350 + function Foo() {
1351 + React.useEffect(() => {
1352 + create++;
1353 + console.log('foo create ' + create);
1354 + return () => {
1355 + cleanup++;
1356 + console.log('foo cleanup ' + cleanup);
1357 + };
1358 + });
1359 + return null;
1360 + }
1361 +
1362 + const container = document.createElement('div');
1363 + const root = ReactDOMClient.createRoot(container);
1364 + await act(() => {
1365 + root.render(
1366 + <React.StrictMode>
1367 + <Foo />
1368 + </React.StrictMode>,
1369 + );
1370 + });
1371 + expect(create).toBe(__DEV__ ? 2 : 1);
1372 + expect(cleanup).toBe(__DEV__ ? 1 : 0);
1373 + expect(console.log).toBeCalledTimes(__DEV__ ? 3 : 1);
1374 + // Note: we should display the first log because otherwise
1375 + // there is a risk of suppressing warnings when they happen,
1376 + // and on the next render they'd get deduplicated and ignored.
1377 + expect(console.log).toBeCalledWith('foo create 1');
1378 + if (__DEV__) {
1379 + expect(console.log).toBeCalledWith('foo cleanup 1');
1380 + }
1381 + });
1382 } else {
1383 it('disable logs for class double render', async () => {
1384 let count = 0;
@@ -1530,6 +1566,39 @@ describe('context legacy', () => {
1566 // and on the next render they'd get deduplicated and ignored.
1567 expect(console.log).toBeCalledWith('foo 1');
1568 });
1569 +
1570 + it('disable logs for effect double invoke', async () => {
1571 + let create = 0;
1572 + let cleanup = 0;
1573 + function Foo() {
1574 + React.useEffect(() => {
1575 + create++;
1576 + console.log('foo create ' + create);
1577 + return () => {
1578 + cleanup++;
1579 + console.log('foo cleanup ' + cleanup);
1580 + };
1581 + });
1582 + return null;
1583 + }
1584 +
1585 + const container = document.createElement('div');
1586 + const root = ReactDOMClient.createRoot(container);
1587 + await act(() => {
1588 + root.render(
1589 + <React.StrictMode>
1590 + <Foo />
1591 + </React.StrictMode>,
1592 + );
1593 + });
1594 + expect(create).toBe(__DEV__ ? 2 : 1);
1595 + expect(cleanup).toBe(__DEV__ ? 1 : 0);
1596 + expect(console.log).toBeCalledTimes(1);
1597 + // Note: we should display the first log because otherwise
1598 + // there is a risk of suppressing warnings when they happen,
1599 + // and on the next render they'd get deduplicated and ignored.
1600 + expect(console.log).toBeCalledWith('foo create 1');
1601 + });
1602 }
1603 });
1604 });