Console Logging for StrictMode Double Rendering (#22030)
React currently suppress console logs in StrictMode during double rendering. However, this causes a lot of confusion. This PR moves the console suppression logic from React into React Devtools. Now by default, we no longer suppress console logs. Instead, we gray out the logs in console during double render. We also add a setting in React Devtools to allow developers to hide console logs during double render if they choose.
Luna Ruan committed
Aug 25, 2021 at 15:35 UTC
60a30cf32e1f974b82f0c7da9d8a03822e5b5208
49 files changed
+799
-180
packages/react-devtools-core/src/standalone.js
+4
@@ -21,6 +21,7 @@ import {
21
getBreakOnConsoleErrors,
22
getSavedComponentFilters,
23
getShowInlineWarningsAndErrors,
24
+ getHideConsoleLogsInStrictMode,
25
} from 'react-devtools-shared/src/utils';
26
import {Server} from 'ws';
27
import {join} from 'path';
@@ -310,6 +311,9 @@ function startServer(
311
)};
312
window.__REACT_DEVTOOLS_SHOW_INLINE_WARNINGS_AND_ERRORS__ = ${JSON.stringify(
313
getShowInlineWarningsAndErrors(),
314
+ )};
315
+ window.__REACT_DEVTOOLS_HIDE_CONSOLE_LOGS_IN_STRICT_MODE__ = ${JSON.stringify(
316
+ getHideConsoleLogsInStrictMode(),
317
)};`;
318
319
response.end(
packages/react-devtools-core/webpack.backend.js
+12
@@ -1,6 +1,12 @@
1
const {resolve} = require('path');
2
const {DefinePlugin} = require('webpack');
3
const {
4
+ DARK_MODE_DIMMED_WARNING_COLOR,
5
+ DARK_MODE_DIMMED_ERROR_COLOR,
6
+ DARK_MODE_DIMMED_LOG_COLOR,
7
+ LIGHT_MODE_DIMMED_WARNING_COLOR,
8
+ LIGHT_MODE_DIMMED_ERROR_COLOR,
9
+ LIGHT_MODE_DIMMED_LOG_COLOR,
10
GITHUB_URL,
11
getVersionString,
12
} = require('react-devtools-extensions/utils');
@@ -60,6 +66,12 @@ module.exports = {
66
'process.env.DEVTOOLS_PACKAGE': `"react-devtools-core"`,
67
'process.env.DEVTOOLS_VERSION': `"${DEVTOOLS_VERSION}"`,
68
'process.env.GITHUB_URL': `"${GITHUB_URL}"`,
69
+ 'process.env.DARK_MODE_DIMMED_WARNING_COLOR': `"${DARK_MODE_DIMMED_WARNING_COLOR}"`,
70
+ 'process.env.DARK_MODE_DIMMED_ERROR_COLOR': `"${DARK_MODE_DIMMED_ERROR_COLOR}"`,
71
+ 'process.env.DARK_MODE_DIMMED_LOG_COLOR': `"${DARK_MODE_DIMMED_LOG_COLOR}"`,
72
+ 'process.env.LIGHT_MODE_DIMMED_WARNING_COLOR': `"${LIGHT_MODE_DIMMED_WARNING_COLOR}"`,
73
+ 'process.env.LIGHT_MODE_DIMMED_ERROR_COLOR': `"${LIGHT_MODE_DIMMED_ERROR_COLOR}"`,
74
+ 'process.env.LIGHT_MODE_DIMMED_LOG_COLOR': `"${LIGHT_MODE_DIMMED_LOG_COLOR}"`,
75
}),
76
],
77
optimization: {
packages/react-devtools-core/webpack.standalone.js
+12
@@ -1,6 +1,12 @@
1
const {resolve} = require('path');
2
const {DefinePlugin} = require('webpack');
3
const {
4
+ DARK_MODE_DIMMED_WARNING_COLOR,
5
+ DARK_MODE_DIMMED_ERROR_COLOR,
6
+ DARK_MODE_DIMMED_LOG_COLOR,
7
+ LIGHT_MODE_DIMMED_WARNING_COLOR,
8
+ LIGHT_MODE_DIMMED_ERROR_COLOR,
9
+ LIGHT_MODE_DIMMED_LOG_COLOR,
10
GITHUB_URL,
11
getVersionString,
12
} = require('react-devtools-extensions/utils');
@@ -67,6 +73,12 @@ module.exports = {
73
'process.env.DEVTOOLS_VERSION': `"${DEVTOOLS_VERSION}"`,
74
'process.env.GITHUB_URL': `"${GITHUB_URL}"`,
75
'process.env.NODE_ENV': `"${NODE_ENV}"`,
76
+ 'process.env.DARK_MODE_DIMMED_WARNING_COLOR': `"${DARK_MODE_DIMMED_WARNING_COLOR}"`,
77
+ 'process.env.DARK_MODE_DIMMED_ERROR_COLOR': `"${DARK_MODE_DIMMED_ERROR_COLOR}"`,
78
+ 'process.env.DARK_MODE_DIMMED_LOG_COLOR': `"${DARK_MODE_DIMMED_LOG_COLOR}"`,
79
+ 'process.env.LIGHT_MODE_DIMMED_WARNING_COLOR': `"${LIGHT_MODE_DIMMED_WARNING_COLOR}"`,
80
+ 'process.env.LIGHT_MODE_DIMMED_ERROR_COLOR': `"${LIGHT_MODE_DIMMED_ERROR_COLOR}"`,
81
+ 'process.env.LIGHT_MODE_DIMMED_LOG_COLOR': `"${LIGHT_MODE_DIMMED_LOG_COLOR}"`,
82
}),
83
],
84
module: {
packages/react-devtools-extensions/src/injectGlobalHook.js
+3
@@ -88,6 +88,9 @@ if (sessionStorageGetItem(SESSION_STORAGE_RELOAD_AND_PROFILE_KEY) === 'true') {
88
89
// Inject a __REACT_DEVTOOLS_GLOBAL_HOOK__ global for React to interact with.
90
// Only do this for HTML documents though, to avoid e.g. breaking syntax highlighting for XML docs.
91
+// We need to inject this code because content scripts (ie injectGlobalHook.js) don't have access
92
+// to the webpage's window, so in order to access front end settings
93
+// and communicate with React, we must inject this code into the webpage
94
if ('text/html' === document.contentType) {
95
injectCode(
96
';(' +
packages/react-devtools-extensions/src/main.js
+7
@@ -11,6 +11,7 @@ import {
11
getBreakOnConsoleErrors,
12
getSavedComponentFilters,
13
getShowInlineWarningsAndErrors,
14
+ getHideConsoleLogsInStrictMode,
15
} from 'react-devtools-shared/src/utils';
16
import {
17
localStorageGetItem,
@@ -42,6 +43,12 @@ function syncSavedPreferences() {
43
)};
44
window.__REACT_DEVTOOLS_SHOW_INLINE_WARNINGS_AND_ERRORS__ = ${JSON.stringify(
45
getShowInlineWarningsAndErrors(),
46
+ )};
47
+ window.__REACT_DEVTOOLS_HIDE_CONSOLE_LOGS_IN_STRICT_MODE__ = ${JSON.stringify(
48
+ getHideConsoleLogsInStrictMode(),
49
+ )};
50
+ window.__REACT_DEVTOOLS_BROWSER_THEME__ = ${JSON.stringify(
51
+ getBrowserTheme(),
52
)};`,
53
);
54
}
packages/react-devtools-extensions/src/utils.js
+2
-2
@@ -1,5 +1,7 @@
1
/* global chrome */
2
3
+import type {BrowserTheme} from 'react-devtools-shared/src/devtools/views/DevTools';
4
+
5
const IS_CHROME = navigator.userAgent.indexOf('Firefox') < 0;
6
7
export type BrowserName = 'Chrome' | 'Firefox';
@@ -8,8 +10,6 @@ export function getBrowserName(): BrowserName {
10
return IS_CHROME ? 'Chrome' : 'Firefox';
11
}
12
11
-export type BrowserTheme = 'dark' | 'light';
12
-
13
export function getBrowserTheme(): BrowserTheme {
14
if (IS_CHROME) {
15
// chrome.devtools.panels added in Chrome 18.
packages/react-devtools-extensions/utils.js
+13
@@ -9,6 +9,13 @@ const {execSync} = require('child_process');
9
const {readFileSync} = require('fs');
10
const {resolve} = require('path');
11
12
+const DARK_MODE_DIMMED_WARNING_COLOR = 'rgba(250, 180, 50, 0.5)';
13
+const DARK_MODE_DIMMED_ERROR_COLOR = 'rgba(250, 123, 130, 0.5)';
14
+const DARK_MODE_DIMMED_LOG_COLOR = 'rgba(125, 125, 125, 0.5)';
15
+const LIGHT_MODE_DIMMED_WARNING_COLOR = 'rgba(250, 180, 50, 0.75)';
16
+const LIGHT_MODE_DIMMED_ERROR_COLOR = 'rgba(250, 123, 130, 0.75)';
17
+const LIGHT_MODE_DIMMED_LOG_COLOR = 'rgba(125, 125, 125, 0.75)';
18
+
19
const GITHUB_URL = 'https://github.com/facebook/react';
20
21
function getGitCommit() {
@@ -36,6 +43,12 @@ function getVersionString() {
43
}
44
45
module.exports = {
46
+ DARK_MODE_DIMMED_WARNING_COLOR,
47
+ DARK_MODE_DIMMED_ERROR_COLOR,
48
+ DARK_MODE_DIMMED_LOG_COLOR,
49
+ LIGHT_MODE_DIMMED_WARNING_COLOR,
50
+ LIGHT_MODE_DIMMED_ERROR_COLOR,
51
+ LIGHT_MODE_DIMMED_LOG_COLOR,
52
GITHUB_URL,
53
getGitCommit,
54
getVersionString,
packages/react-devtools-extensions/webpack.backend.js
+17
-2
@@ -2,7 +2,16 @@
2
3
const {resolve} = require('path');
4
const {DefinePlugin} = require('webpack');
5
-const {GITHUB_URL, getVersionString} = require('./utils');
5
+const {
6
+ DARK_MODE_DIMMED_WARNING_COLOR,
7
+ DARK_MODE_DIMMED_ERROR_COLOR,
8
+ DARK_MODE_DIMMED_LOG_COLOR,
9
+ LIGHT_MODE_DIMMED_WARNING_COLOR,
10
+ LIGHT_MODE_DIMMED_ERROR_COLOR,
11
+ LIGHT_MODE_DIMMED_LOG_COLOR,
12
+ GITHUB_URL,
13
+ getVersionString,
14
+} = require('./utils');
15
const {resolveFeatureFlags} = require('react-devtools-shared/buildUtils');
16
17
const NODE_ENV = process.env.NODE_ENV;
@@ -54,10 +63,16 @@ module.exports = {
63
new DefinePlugin({
64
__DEV__: true,
65
__PROFILE__: false,
57
- __EXPERIMENTAL__: true,
66
+ __DEV____DEV__: true,
67
'process.env.DEVTOOLS_PACKAGE': `"react-devtools-extensions"`,
68
'process.env.DEVTOOLS_VERSION': `"${DEVTOOLS_VERSION}"`,
69
'process.env.GITHUB_URL': `"${GITHUB_URL}"`,
70
+ 'process.env.DARK_MODE_DIMMED_WARNING_COLOR': `"${DARK_MODE_DIMMED_WARNING_COLOR}"`,
71
+ 'process.env.DARK_MODE_DIMMED_ERROR_COLOR': `"${DARK_MODE_DIMMED_ERROR_COLOR}"`,
72
+ 'process.env.DARK_MODE_DIMMED_LOG_COLOR': `"${DARK_MODE_DIMMED_LOG_COLOR}"`,
73
+ 'process.env.LIGHT_MODE_DIMMED_WARNING_COLOR': `"${LIGHT_MODE_DIMMED_WARNING_COLOR}"`,
74
+ 'process.env.LIGHT_MODE_DIMMED_ERROR_COLOR': `"${LIGHT_MODE_DIMMED_ERROR_COLOR}"`,
75
+ 'process.env.LIGHT_MODE_DIMMED_LOG_COLOR': `"${LIGHT_MODE_DIMMED_LOG_COLOR}"`,
76
}),
77
],
78
module: {
packages/react-devtools-extensions/webpack.config.js
+16
-1
@@ -2,7 +2,16 @@
2
3
const {resolve} = require('path');
4
const {DefinePlugin} = require('webpack');
5
-const {GITHUB_URL, getVersionString} = require('./utils');
5
+const {
6
+ DARK_MODE_DIMMED_WARNING_COLOR,
7
+ DARK_MODE_DIMMED_ERROR_COLOR,
8
+ DARK_MODE_DIMMED_LOG_COLOR,
9
+ LIGHT_MODE_DIMMED_WARNING_COLOR,
10
+ LIGHT_MODE_DIMMED_ERROR_COLOR,
11
+ LIGHT_MODE_DIMMED_LOG_COLOR,
12
+ GITHUB_URL,
13
+ getVersionString,
14
+} = require('./utils');
15
const {resolveFeatureFlags} = require('react-devtools-shared/buildUtils');
16
17
const NODE_ENV = process.env.NODE_ENV;
@@ -76,6 +85,12 @@ module.exports = {
85
'process.env.DEVTOOLS_VERSION': `"${DEVTOOLS_VERSION}"`,
86
'process.env.GITHUB_URL': `"${GITHUB_URL}"`,
87
'process.env.NODE_ENV': `"${NODE_ENV}"`,
88
+ 'process.env.DARK_MODE_DIMMED_WARNING_COLOR': `"${DARK_MODE_DIMMED_WARNING_COLOR}"`,
89
+ 'process.env.DARK_MODE_DIMMED_ERROR_COLOR': `"${DARK_MODE_DIMMED_ERROR_COLOR}"`,
90
+ 'process.env.DARK_MODE_DIMMED_LOG_COLOR': `"${DARK_MODE_DIMMED_LOG_COLOR}"`,
91
+ 'process.env.LIGHT_MODE_DIMMED_WARNING_COLOR': `"${LIGHT_MODE_DIMMED_WARNING_COLOR}"`,
92
+ 'process.env.LIGHT_MODE_DIMMED_ERROR_COLOR': `"${LIGHT_MODE_DIMMED_ERROR_COLOR}"`,
93
+ 'process.env.LIGHT_MODE_DIMMED_LOG_COLOR': `"${LIGHT_MODE_DIMMED_LOG_COLOR}"`,
94
}),
95
],
96
module: {
packages/react-devtools-inline/README.md
+5
-4
@@ -83,14 +83,15 @@ const { contentWindow } = iframe;
83
// This must be called before React is loaded into that frame.
84
initializeBackend(contentWindow);
85
86
-// React application can be injected into <iframe> at any time now...
87
-// Note that this would need to be done via <script> tag injection,
88
-// as setting the src of the <iframe> would load a new page (without the injected backend).
89
-
86
// Initialize DevTools UI to listen to the hook we just installed.
87
// This returns a React component we can render anywhere in the parent window.
88
+// This also must be called before React is loaded into the iframe
89
const DevTools = initializeFrontend(contentWindow);
90
91
+// React application can be injected into <iframe> at any time now...
92
+// Note that this would need to be done via <script> tag injection,
93
+// as setting the src of the <iframe> would load a new page (without the injected backend).
94
+
95
// <DevTools /> interface can be rendered in the parent window at any time now...
96
// Be sure to use either ReactDOM.createRoot()
97
// or ReactDOM.createSyncRoot() to render this component.
packages/react-devtools-inline/src/backend.js
+3
@@ -25,12 +25,14 @@ function startActivation(contentWindow: window) {
25
breakOnConsoleErrors,
26
componentFilters,
27
showInlineWarningsAndErrors,
28
+ hideConsoleLogsInStrictMode,
29
} = data;
30
31
contentWindow.__REACT_DEVTOOLS_APPEND_COMPONENT_STACK__ = appendComponentStack;
32
contentWindow.__REACT_DEVTOOLS_BREAK_ON_CONSOLE_ERRORS__ = breakOnConsoleErrors;
33
contentWindow.__REACT_DEVTOOLS_COMPONENT_FILTERS__ = componentFilters;
34
contentWindow.__REACT_DEVTOOLS_SHOW_INLINE_WARNINGS_AND_ERRORS__ = showInlineWarningsAndErrors;
35
+ contentWindow.__REACT_DEVTOOLS_HIDE_CONSOLE_LOGS_IN_STRICT_MODE__ = hideConsoleLogsInStrictMode;
36
37
// TRICKY
38
// The backend entry point may be required in the context of an iframe or the parent window.
@@ -43,6 +45,7 @@ function startActivation(contentWindow: window) {
45
window.__REACT_DEVTOOLS_BREAK_ON_CONSOLE_ERRORS__ = breakOnConsoleErrors;
46
window.__REACT_DEVTOOLS_COMPONENT_FILTERS__ = componentFilters;
47
window.__REACT_DEVTOOLS_SHOW_INLINE_WARNINGS_AND_ERRORS__ = showInlineWarningsAndErrors;
48
+ window.__REACT_DEVTOOLS_HIDE_CONSOLE_LOGS_IN_STRICT_MODE__ = hideConsoleLogsInStrictMode;
49
}
50
51
finishActivation(contentWindow);
packages/react-devtools-inline/src/frontend.js
+2
@@ -10,6 +10,7 @@ import {
10
getBreakOnConsoleErrors,
11
getSavedComponentFilters,
12
getShowInlineWarningsAndErrors,
13
+ getHideConsoleLogsInStrictMode,
14
} from 'react-devtools-shared/src/utils';
15
import {
16
MESSAGE_TYPE_GET_SAVED_PREFERENCES,
@@ -88,6 +89,7 @@ export function initialize(
89
breakOnConsoleErrors: getBreakOnConsoleErrors(),
90
componentFilters: getSavedComponentFilters(),
91
showInlineWarningsAndErrors: getShowInlineWarningsAndErrors(),
92
+ hideConsoleLogsInStrictMode: getHideConsoleLogsInStrictMode(),
93
},
94
'*',
95
);
packages/react-devtools-inline/webpack.config.js
+12
@@ -1,6 +1,12 @@
1
const {resolve} = require('path');
2
const {DefinePlugin} = require('webpack');
3
const {
4
+ DARK_MODE_DIMMED_WARNING_COLOR,
5
+ DARK_MODE_DIMMED_ERROR_COLOR,
6
+ DARK_MODE_DIMMED_LOG_COLOR,
7
+ LIGHT_MODE_DIMMED_WARNING_COLOR,
8
+ LIGHT_MODE_DIMMED_ERROR_COLOR,
9
+ LIGHT_MODE_DIMMED_LOG_COLOR,
10
GITHUB_URL,
11
getVersionString,
12
} = require('react-devtools-extensions/utils');
@@ -70,6 +76,12 @@ module.exports = {
76
'process.env.DEVTOOLS_VERSION': `"${DEVTOOLS_VERSION}"`,
77
'process.env.GITHUB_URL': `"${GITHUB_URL}"`,
78
'process.env.NODE_ENV': `"${NODE_ENV}"`,
79
+ 'process.env.DARK_MODE_DIMMED_WARNING_COLOR': `"${DARK_MODE_DIMMED_WARNING_COLOR}"`,
80
+ 'process.env.DARK_MODE_DIMMED_ERROR_COLOR': `"${DARK_MODE_DIMMED_ERROR_COLOR}"`,
81
+ 'process.env.DARK_MODE_DIMMED_LOG_COLOR': `"${DARK_MODE_DIMMED_LOG_COLOR}"`,
82
+ 'process.env.LIGHT_MODE_DIMMED_WARNING_COLOR': `"${LIGHT_MODE_DIMMED_WARNING_COLOR}"`,
83
+ 'process.env.LIGHT_MODE_DIMMED_ERROR_COLOR': `"${LIGHT_MODE_DIMMED_ERROR_COLOR}"`,
84
+ 'process.env.LIGHT_MODE_DIMMED_LOG_COLOR': `"${LIGHT_MODE_DIMMED_LOG_COLOR}"`,
85
}),
86
],
87
module: {
packages/react-devtools-shared/src/__tests__/console-test.js
+89
-2
@@ -9,6 +9,7 @@
9
10
describe('console', () => {
11
let React;
12
+ let ReactDOM;
13
let act;
14
let fakeConsole;
15
let legacyRender;
@@ -48,6 +49,7 @@ describe('console', () => {
49
appendComponentStack: true,
50
breakOnWarn: false,
51
showInlineWarningsAndErrors: false,
52
+ hideDoubleLogsInStrictLegacy: false,
53
});
54
55
const inject = global.__REACT_DEVTOOLS_GLOBAL_HOOK__.inject;
@@ -58,6 +60,7 @@ describe('console', () => {
60
};
61
62
React = require('react');
63
+ ReactDOM = require('react-dom');
64
65
const utils = require('./utils');
66
act = utils.act;
@@ -73,10 +76,10 @@ describe('console', () => {
76
);
77
}
78
76
- it('should not patch console methods that do not receive component stacks', () => {
79
+ it('should not patch console methods that are not explicitly overriden', () => {
80
expect(fakeConsole.error).not.toBe(mockError);
81
expect(fakeConsole.info).toBe(mockInfo);
79
- expect(fakeConsole.log).toBe(mockLog);
82
+ expect(fakeConsole.log).not.toBe(mockLog);
83
expect(fakeConsole.warn).not.toBe(mockWarn);
84
});
85
@@ -465,4 +468,88 @@ describe('console', () => {
468
expect(mockWarn).toHaveBeenCalledTimes(1);
469
expect(mockWarn.mock.calls[0][0]).toBe('Symbol:');
470
});
471
+
472
+ it('should double log if hideConsoleLogsInStrictMode is disabled in Strict mode', () => {
473
+ const container = document.createElement('div');
474
+ const root = ReactDOM.createRoot(container);
475
+
476
+ function App() {
477
+ fakeConsole.log('log');
478
+ fakeConsole.warn('warn');
479
+ fakeConsole.error('error');
480
+ return <div />;
481
+ }
482
+
483
+ patchConsole({
484
+ appendComponentStack: false,
485
+ breakOnWarn: false,
486
+ showInlineWarningsAndErrors: false,
487
+ hideConsoleLogsInStrictMode: false,
488
+ });
489
+
490
+ act(() =>
491
+ root.render(
492
+ <React.StrictMode>
493
+ <App />
494
+ </React.StrictMode>,
495
+ ),
496
+ );
497
+
498
+ expect(mockLog).toHaveBeenCalledTimes(2);
499
+ expect(mockLog.mock.calls[0]).toHaveLength(1);
500
+ expect(mockLog.mock.calls[0][0]).toBe('log');
501
+ expect(mockLog.mock.calls[1]).toHaveLength(2);
502
+ expect(mockLog.mock.calls[1][0]).toBe('%clog');
503
+
504
+ expect(mockWarn).toHaveBeenCalledTimes(2);
505
+ expect(mockWarn.mock.calls[0]).toHaveLength(1);
506
+ expect(mockWarn.mock.calls[0][0]).toBe('warn');
507
+ expect(mockWarn.mock.calls[1]).toHaveLength(2);
508
+ expect(mockWarn.mock.calls[1][0]).toBe('%cwarn');
509
+
510
+ expect(mockError).toHaveBeenCalledTimes(2);
511
+ expect(mockError.mock.calls[0]).toHaveLength(1);
512
+ expect(mockError.mock.calls[0][0]).toBe('error');
513
+ expect(mockError.mock.calls[1]).toHaveLength(2);
514
+ expect(mockError.mock.calls[1][0]).toBe('%cerror');
515
+ });
516
+
517
+ it('should not double log if hideConsoleLogsInStrictMode is enabled in Strict mode', () => {
518
+ const container = document.createElement('div');
519
+ const root = ReactDOM.createRoot(container);
520
+
521
+ function App() {
522
+ fakeConsole.log('log');
523
+ fakeConsole.warn('warn');
524
+ fakeConsole.error('error');
525
+ return <div />;
526
+ }
527
+
528
+ patchConsole({
529
+ appendComponentStack: false,
530
+ breakOnWarn: false,
531
+ showInlineWarningsAndErrors: false,
532
+ hideConsoleLogsInStrictMode: true,
533
+ });
534
+
535
+ act(() =>
536
+ root.render(
537
+ <React.StrictMode>
538
+ <App />
539
+ </React.StrictMode>,
540
+ ),
541
+ );
542
+
543
+ expect(mockLog).toHaveBeenCalledTimes(1);
544
+ expect(mockLog.mock.calls[0]).toHaveLength(1);
545
+ expect(mockLog.mock.calls[0][0]).toBe('log');
546
+
547
+ expect(mockWarn).toHaveBeenCalledTimes(1);
548
+ expect(mockWarn.mock.calls[0]).toHaveLength(1);
549
+ expect(mockWarn.mock.calls[0][0]).toBe('warn');
550
+
551
+ expect(mockError).toHaveBeenCalledTimes(1);
552
+ expect(mockError.mock.calls[0]).toHaveLength(1);
553
+ expect(mockError.mock.calls[0][0]).toBe('error');
554
+ });
555
});
packages/react-devtools-shared/src/__tests__/setupEnv.js
+16
@@ -1,5 +1,14 @@
1
'use strict';
2
3
+const {
4
+ DARK_MODE_DIMMED_WARNING_COLOR,
5
+ DARK_MODE_DIMMED_ERROR_COLOR,
6
+ DARK_MODE_DIMMED_LOG_COLOR,
7
+ LIGHT_MODE_DIMMED_WARNING_COLOR,
8
+ LIGHT_MODE_DIMMED_ERROR_COLOR,
9
+ LIGHT_MODE_DIMMED_LOG_COLOR,
10
+} = require('react-devtools-extensions/utils');
11
+
12
// DevTools stores preferences between sessions in localStorage
13
if (!global.hasOwnProperty('localStorage')) {
14
global.localStorage = require('local-storage-fallback').default;
@@ -8,3 +17,10 @@ if (!global.hasOwnProperty('localStorage')) {
17
// Mimic the global we set with Webpack's DefinePlugin
18
global.__DEV__ = process.env.NODE_ENV !== 'production';
19
global.__TEST__ = true;
20
+
21
+global.process.env.DARK_MODE_DIMMED_WARNING_COLOR = DARK_MODE_DIMMED_WARNING_COLOR;
22
+global.process.env.DARK_MODE_DIMMED_ERROR_COLOR = DARK_MODE_DIMMED_ERROR_COLOR;
23
+global.process.env.DARK_MODE_DIMMED_LOG_COLOR = DARK_MODE_DIMMED_LOG_COLOR;
24
+global.process.env.LIGHT_MODE_DIMMED_WARNING_COLOR = LIGHT_MODE_DIMMED_WARNING_COLOR;
25
+global.process.env.LIGHT_MODE_DIMMED_ERROR_COLOR = LIGHT_MODE_DIMMED_ERROR_COLOR;
26
+global.process.env.LIGHT_MODE_DIMMED_LOG_COLOR = LIGHT_MODE_DIMMED_LOG_COLOR;
packages/react-devtools-shared/src/backend/agent.js
+14
-15
@@ -25,7 +25,7 @@ import {
25
initialize as setupTraceUpdates,
26
toggleEnabled as setTraceUpdatesEnabled,
27
} from './views/TraceUpdates';
28
-import {patch as patchConsole, unpatch as unpatchConsole} from './console';
28
+import {patch as patchConsole} from './console';
29
import {currentBridgeProtocol} from 'react-devtools-shared/src/bridge';
30
31
import type {BackendBridge} from 'react-devtools-shared/src/bridge';
@@ -40,6 +40,7 @@ import type {
40
} from './types';
41
import type {ComponentFilter} from '../types';
42
import {isSynchronousXHRSupported} from './utils';
43
+import type {BrowserTheme} from 'react-devtools-shared/src/devtools/views/DevTools';
44
45
const debug = (methodName, ...args) => {
46
if (__DEBUG__) {
@@ -635,28 +636,26 @@ export default class Agent extends EventEmitter<{|
636
appendComponentStack,
637
breakOnConsoleErrors,
638
showInlineWarningsAndErrors,
639
+ hideConsoleLogsInStrictMode,
640
+ browserTheme,
641
}: {|
642
appendComponentStack: boolean,
643
breakOnConsoleErrors: boolean,
644
showInlineWarningsAndErrors: boolean,
645
+ hideConsoleLogsInStrictMode: boolean,
646
+ browserTheme: BrowserTheme,
647
|}) => {
648
// If the frontend preference has change,
649
// or in the case of React Native- if the backend is just finding out the preference-
645
- // then install or uninstall the console overrides.
650
+ // then reinstall the console overrides.
651
// It's safe to call these methods multiple times, so we don't need to worry about that.
647
- if (
648
- appendComponentStack ||
649
- breakOnConsoleErrors ||
650
- showInlineWarningsAndErrors
651
- ) {
652
- patchConsole({
653
- appendComponentStack,
654
- breakOnConsoleErrors,
655
- showInlineWarningsAndErrors,
656
- });
657
- } else {
658
- unpatchConsole();
659
- }
652
+ patchConsole({
653
+ appendComponentStack,
654
+ breakOnConsoleErrors,
655
+ showInlineWarningsAndErrors,
656
+ hideConsoleLogsInStrictMode,
657
+ browserTheme,
658
+ });
659
};
660
661
updateComponentFilters = (componentFilters: Array<ComponentFilter>) => {
packages/react-devtools-shared/src/backend/console.js
+116
-49
@@ -9,11 +9,14 @@
9
10
import type {Fiber} from 'react-reconciler/src/ReactInternalTypes';
11
import type {CurrentDispatcherRef, ReactRenderer, WorkTagMap} from './types';
12
+import type {BrowserTheme} from 'react-devtools-shared/src/devtools/views/DevTools';
13
+import {format} from './utils';
14
15
import {getInternalReactConstants} from './renderer';
16
import {getStackByFiberInDevAndProd} from './DevToolsFiberComponentStack';
17
16
-const APPEND_STACK_TO_METHODS = ['error', 'trace', 'warn'];
18
+const OVERRIDE_CONSOLE_METHODS = ['error', 'trace', 'warn', 'log'];
19
+const DIMMED_NODE_CONSOLE_COLOR = '\x1b[2m%s\x1b[0m';
20
21
// React's custom built component stack strings match "\s{4}in"
22
// Chrome's prefix matches "\s{4}at"
@@ -39,6 +42,7 @@ const injectedRenderers: Map<
42
getCurrentFiber: () => Fiber | null,
43
onErrorOrWarning: ?OnErrorOrWarning,
44
workTagMap: WorkTagMap,
45
+ getIsStrictMode: ?() => boolean,
46
|},
47
> = new Map();
48
@@ -50,6 +54,11 @@ for (const method in console) {
54
55
let unpatchFn: null | (() => void) = null;
56
57
+let isNode = false;
58
+try {
59
+ isNode = this === global;
60
+} catch (error) {}
61
+
62
// Enables e.g. Jest tests to inject a mock console object.
63
export function dangerous_setTargetConsoleForTesting(
64
targetConsoleForTesting: Object,
@@ -72,6 +81,7 @@ export function registerRenderer(
81
const {
82
currentDispatcherRef,
83
getCurrentFiber,
84
+ getIsStrictMode,
85
findFiberByHostInstance,
86
version,
87
} = renderer;
@@ -89,6 +99,7 @@ export function registerRenderer(
99
injectedRenderers.set(renderer, {
100
currentDispatcherRef,
101
getCurrentFiber,
102
+ getIsStrictMode,
103
workTagMap: ReactTypeOfWork,
104
onErrorOrWarning,
105
});
@@ -99,24 +110,31 @@ const consoleSettingsRef = {
110
appendComponentStack: false,
111
breakOnConsoleErrors: false,
112
showInlineWarningsAndErrors: false,
113
+ hideConsoleLogsInStrictMode: false,
114
};
115
116
// Patches console methods to append component stack for the current fiber.
117
// Call unpatch() to remove the injected behavior.
118
+// NOTE: KEEP IN SYNC with src/hook.js:patchConsoleForInitialRenderInExtension
119
export function patch({
120
appendComponentStack,
121
breakOnConsoleErrors,
122
showInlineWarningsAndErrors,
123
+ hideConsoleLogsInStrictMode,
124
+ browserTheme,
125
}: {
126
appendComponentStack: boolean,
127
breakOnConsoleErrors: boolean,
128
showInlineWarningsAndErrors: boolean,
129
+ hideConsoleLogsInStrictMode: boolean,
130
+ browserTheme: BrowserTheme,
131
}): void {
132
// Settings may change after we've patched the console.
133
// Using a shared ref allows the patch function to read the latest values.
134
consoleSettingsRef.appendComponentStack = appendComponentStack;
135
consoleSettingsRef.breakOnConsoleErrors = breakOnConsoleErrors;
136
consoleSettingsRef.showInlineWarningsAndErrors = showInlineWarningsAndErrors;
137
+ consoleSettingsRef.hideConsoleLogsInStrictMode = hideConsoleLogsInStrictMode;
138
139
if (unpatchFn !== null) {
140
// Don't patch twice.
@@ -134,68 +152,78 @@ export function patch({
152
}
153
};
154
137
- APPEND_STACK_TO_METHODS.forEach(method => {
155
+ OVERRIDE_CONSOLE_METHODS.forEach(method => {
156
try {
139
- const originalMethod = (originalConsoleMethods[method] =
140
- targetConsole[method]);
157
+ const originalMethod = (originalConsoleMethods[method] = targetConsole[
158
+ method
159
+ ].__REACT_DEVTOOLS_ORIGINAL_METHOD__
160
+ ? targetConsole[method].__REACT_DEVTOOLS_ORIGINAL_METHOD__
161
+ : targetConsole[method]);
162
163
const overrideMethod = (...args) => {
164
let shouldAppendWarningStack = false;
144
- if (consoleSettingsRef.appendComponentStack) {
145
- const lastArg = args.length > 0 ? args[args.length - 1] : null;
146
- const alreadyHasComponentStack =
147
- typeof lastArg === 'string' && isStringComponentStack(lastArg);
148
-
149
- // If we are ever called with a string that already has a component stack,
150
- // e.g. a React error/warning, don't append a second stack.
151
- shouldAppendWarningStack = !alreadyHasComponentStack;
165
+ if (method !== 'log') {
166
+ if (consoleSettingsRef.appendComponentStack) {
167
+ const lastArg = args.length > 0 ? args[args.length - 1] : null;
168
+ const alreadyHasComponentStack =
169
+ typeof lastArg === 'string' && isStringComponentStack(lastArg);
170
+
171
+ // If we are ever called with a string that already has a component stack,
172
+ // e.g. a React error/warning, don't append a second stack.
173
+ shouldAppendWarningStack = !alreadyHasComponentStack;
174
+ }
175
}
176
177
const shouldShowInlineWarningsAndErrors =
178
consoleSettingsRef.showInlineWarningsAndErrors &&
179
(method === 'error' || method === 'warn');
180
158
- if (shouldAppendWarningStack || shouldShowInlineWarningsAndErrors) {
159
- // Search for the first renderer that has a current Fiber.
160
- // We don't handle the edge case of stacks for more than one (e.g. interleaved renderers?)
161
- // eslint-disable-next-line no-for-of-loops/no-for-of-loops
162
- for (const {
163
- currentDispatcherRef,
164
- getCurrentFiber,
165
- onErrorOrWarning,
166
- workTagMap,
167
- } of injectedRenderers.values()) {
168
- const current: ?Fiber = getCurrentFiber();
169
- if (current != null) {
170
- try {
171
- if (shouldShowInlineWarningsAndErrors) {
172
- // patch() is called by two places: (1) the hook and (2) the renderer backend.
173
- // The backend is what implements a message queue, so it's the only one that injects onErrorOrWarning.
174
- if (typeof onErrorOrWarning === 'function') {
175
- onErrorOrWarning(
176
- current,
177
- ((method: any): 'error' | 'warn'),
178
- // Copy args before we mutate them (e.g. adding the component stack)
179
- args.slice(),
180
- );
181
- }
182
- }
181
+ let isInStrictMode = false;
182
184
- if (shouldAppendWarningStack) {
185
- const componentStack = getStackByFiberInDevAndProd(
186
- workTagMap,
183
+ // Search for the first renderer that has a current Fiber.
184
+ // We don't handle the edge case of stacks for more than one (e.g. interleaved renderers?)
185
+ // eslint-disable-next-line no-for-of-loops/no-for-of-loops
186
+ for (const {
187
+ currentDispatcherRef,
188
+ getCurrentFiber,
189
+ onErrorOrWarning,
190
+ workTagMap,
191
+ getIsStrictMode,
192
+ } of injectedRenderers.values()) {
193
+ const current: ?Fiber = getCurrentFiber();
194
+ if (current != null) {
195
+ try {
196
+ if (typeof getIsStrictMode === 'function' && getIsStrictMode()) {
197
+ isInStrictMode = true;
198
+ }
199
+
200
+ if (shouldShowInlineWarningsAndErrors) {
201
+ // patch() is called by two places: (1) the hook and (2) the renderer backend.
202
+ // The backend is what implements a message queue, so it's the only one that injects onErrorOrWarning.
203
+ if (typeof onErrorOrWarning === 'function') {
204
+ onErrorOrWarning(
205
current,
188
- currentDispatcherRef,
206
+ ((method: any): 'error' | 'warn'),
207
+ // Copy args before we mutate them (e.g. adding the component stack)
208
+ args.slice(),
209
);
190
- if (componentStack !== '') {
191
- args.push(componentStack);
192
- }
210
}
194
- } catch (error) {
195
- // Don't let a DevTools or React internal error interfere with logging.
196
- } finally {
197
- break;
211
}
212
+
213
+ if (shouldAppendWarningStack) {
214
+ const componentStack = getStackByFiberInDevAndProd(
215
+ workTagMap,
216
+ current,
217
+ currentDispatcherRef,
218
+ );
219
+ if (componentStack !== '') {
220
+ args.push(componentStack);
221
+ }
222
+ }
223
+ } catch (error) {
224
+ // Don't let a DevTools or React internal error interfere with logging.
225
+ } finally {
226
+ break;
227
}
228
}
229
}
@@ -209,7 +237,46 @@ export function patch({
237
debugger;
238
}
239
212
- originalMethod(...args);
240
+ if (isInStrictMode) {
241
+ if (!consoleSettingsRef.hideConsoleLogsInStrictMode) {
242
+ // Dim the text color of the double logs if we're not
243
+ // hiding them.
244
+ if (isNode) {
245
+ originalMethod(DIMMED_NODE_CONSOLE_COLOR, format(...args));
246
+ } else {
247
+ let color;
248
+ switch (method) {
249
+ case 'warn':
250
+ color =
251
+ browserTheme === 'light'
252
+ ? process.env.LIGHT_MODE_DIMMED_WARNING_COLOR
253
+ : process.env.DARK_MODE_DIMMED_WARNING_COLOR;
254
+ break;
255
+ case 'error':
256
+ color =
257
+ browserTheme === 'light'
258
+ ? process.env.LIGHT_MODE_DIMMED_ERROR_COLOR
259
+ : process.env.DARK_MODE_DIMMED_ERROR_COLOR;
260
+ break;
261
+ case 'log':
262
+ default:
263
+ color =
264
+ browserTheme === 'light'
265
+ ? process.env.LIGHT_MODE_DIMMED_LOG_COLOR
266
+ : process.env.DARK_MODE_DIMMED_LOG_COLOR;
267
+ break;
268
+ }
269
+
270
+ if (color) {
271
+ originalMethod(`%c${format(...args)}`, `color: ${color}`);
272
+ } else {
273
+ throw Error('Console color is not defined');
274
+ }
275
+ }
276
+ }
277
+ } else {
278
+ originalMethod(...args);
279
+ }
280
};
281
282
overrideMethod.__REACT_DEVTOOLS_ORIGINAL_METHOD__ = originalMethod;
packages/react-devtools-shared/src/backend/renderer.js
+11
-11
@@ -740,17 +740,17 @@ export function attach(
740
window.__REACT_DEVTOOLS_BREAK_ON_CONSOLE_ERRORS__ === true;
741
const showInlineWarningsAndErrors =
742
window.__REACT_DEVTOOLS_SHOW_INLINE_WARNINGS_AND_ERRORS__ !== false;
743
- if (
744
- appendComponentStack ||
745
- breakOnConsoleErrors ||
746
- showInlineWarningsAndErrors
747
- ) {
748
- patchConsole({
749
- appendComponentStack,
750
- breakOnConsoleErrors,
751
- showInlineWarningsAndErrors,
752
- });
753
- }
743
+ const hideConsoleLogsInStrictMode =
744
+ window.__REACT_DEVTOOLS_HIDE_CONSOLE_LOGS_IN_STRICT_MODE__ === true;
745
+ const browserTheme = window.__REACT_DEVTOOLS_BROWSER_THEME__;
746
+
747
+ patchConsole({
748
+ appendComponentStack,
749
+ breakOnConsoleErrors,
750
+ showInlineWarningsAndErrors,
751
+ hideConsoleLogsInStrictMode,
752
+ browserTheme,
753
+ });
754
}
755
756
const debug = (
packages/react-devtools-shared/src/backend/types.js
+2
@@ -136,6 +136,8 @@ export type ReactRenderer = {
136
// Only injected by React v16.9+ in DEV mode.
137
// Enables DevTools to append owners-only component stack to error messages.
138
getCurrentFiber?: () => Fiber | null,
139
+
140
+ getIsStrictMode?: () => boolean,
141
// 17.0.2+
142
reconcilerVersion?: string,
143
// Uniquely identifies React DOM v15.
packages/react-devtools-shared/src/backend/utils.js
+1
@@ -159,6 +159,7 @@ export function serializeToString(data: any): string {
159
// based on https://github.com/tmpfs/format-util/blob/0e62d430efb0a1c51448709abd3e2406c14d8401/format.js#L1
160
// based on https://developer.mozilla.org/en-US/docs/Web/API/console#Using_string_substitutions
161
// Implements s, d, i and f placeholders
162
+// NOTE: KEEP IN SYNC with src/hook.js
163
export function format(
164
maybeMessage: any,
165
...inputArgs: $ReadOnlyArray<any>
packages/react-devtools-shared/src/bridge.js
+3
@@ -17,6 +17,7 @@ import type {
17
RendererID,
18
} from 'react-devtools-shared/src/backend/types';
19
import type {StyleAndLayout as StyleAndLayoutPayload} from 'react-devtools-shared/src/backend/NativeStyleEditor/types';
20
+import type {BrowserTheme} from 'react-devtools-shared/src/devtools/views/DevTools';
21
22
const BATCH_DURATION = 100;
23
@@ -164,6 +165,8 @@ type UpdateConsolePatchSettingsParams = {|
165
appendComponentStack: boolean,
166
breakOnConsoleErrors: boolean,
167
showInlineWarningsAndErrors: boolean,
168
+ hideConsoleLogsInStrictMode: boolean,
169
+ browserTheme: BrowserTheme,
170
|};
171
172
export type BackendEvents = {|
packages/react-devtools-shared/src/constants.js
+3
@@ -47,6 +47,9 @@ export const LOCAL_STORAGE_SHOW_INLINE_WARNINGS_AND_ERRORS_KEY =
47
export const LOCAL_STORAGE_TRACE_UPDATES_ENABLED_KEY =
48
'React::DevTools::traceUpdatesEnabled';
49
50
+export const LOCAL_STORAGE_HIDE_CONSOLE_LOGS_IN_STRICT_MODE =
51
+ 'React::DevTools::hideConsoleLogsInStrictMode';
52
+
53
export const PROFILER_EXPORT_VERSION = 5;
54
55
export const CHANGE_LOG_URL =
packages/react-devtools-shared/src/devtools/views/Settings/DebuggingSettings.js
+13
-2
@@ -17,10 +17,12 @@ export default function DebuggingSettings(_: {||}) {
17
const {
18
appendComponentStack,
19
breakOnConsoleErrors,
20
+ hideConsoleLogsInStrictMode,
21
setAppendComponentStack,
22
setBreakOnConsoleErrors,
23
setShowInlineWarningsAndErrors,
24
showInlineWarningsAndErrors,
25
+ sethideConsoleLogsInStrictMode,
26
} = useContext(SettingsContext);
27
28
return (
@@ -64,8 +66,17 @@ export default function DebuggingSettings(_: {||}) {
66
</label>
67
</div>
68
67
- <div className={styles.ConsoleAPIWarning}>
68
- These settings require DevTools to override native console APIs.
69
+ <div className={styles.Setting}>
70
+ <label>
71
+ <input
72
+ type="checkbox"
73
+ checked={hideConsoleLogsInStrictMode}
74
+ onChange={({currentTarget}) =>
75
+ sethideConsoleLogsInStrictMode(currentTarget.checked)
76
+ }
77
+ />{' '}
78
+ Hide logs during second render in Strict Mode
79
+ </label>
80
</div>
81
</div>
82
);
packages/react-devtools-shared/src/devtools/views/Settings/SettingsContext.js
+19
@@ -23,6 +23,7 @@ import {
23
LOCAL_STORAGE_SHOULD_PATCH_CONSOLE_KEY,
24
LOCAL_STORAGE_TRACE_UPDATES_ENABLED_KEY,
25
LOCAL_STORAGE_SHOW_INLINE_WARNINGS_AND_ERRORS_KEY,
26
+ LOCAL_STORAGE_HIDE_CONSOLE_LOGS_IN_STRICT_MODE,
27
} from 'react-devtools-shared/src/constants';
28
import {useLocalStorage} from '../hooks';
29
import {BridgeContext} from '../context';
@@ -49,6 +50,9 @@ type Context = {|
50
parseHookNames: boolean,
51
setParseHookNames: (value: boolean) => void,
52
53
+ hideConsoleLogsInStrictMode: boolean,
54
+ sethideConsoleLogsInStrictMode: (value: boolean) => void,
55
+
56
showInlineWarningsAndErrors: boolean,
57
setShowInlineWarningsAndErrors: (value: boolean) => void,
58
@@ -104,6 +108,13 @@ function SettingsContextController({
108
LOCAL_STORAGE_PARSE_HOOK_NAMES_KEY,
109
false,
110
);
111
+ const [
112
+ hideConsoleLogsInStrictMode,
113
+ sethideConsoleLogsInStrictMode,
114
+ ] = useLocalStorage<boolean>(
115
+ LOCAL_STORAGE_HIDE_CONSOLE_LOGS_IN_STRICT_MODE,
116
+ false,
117
+ );
118
const [
119
showInlineWarningsAndErrors,
120
setShowInlineWarningsAndErrors,
@@ -169,12 +180,16 @@ function SettingsContextController({
180
appendComponentStack,
181
breakOnConsoleErrors,
182
showInlineWarningsAndErrors,
183
+ hideConsoleLogsInStrictMode,
184
+ browserTheme,
185
});
186
}, [
187
bridge,
188
appendComponentStack,
189
breakOnConsoleErrors,
190
showInlineWarningsAndErrors,
191
+ hideConsoleLogsInStrictMode,
192
+ browserTheme,
193
]);
194
195
useEffect(() => {
@@ -199,6 +214,8 @@ function SettingsContextController({
214
setTraceUpdatesEnabled,
215
setShowInlineWarningsAndErrors,
216
showInlineWarningsAndErrors,
217
+ sethideConsoleLogsInStrictMode,
218
+ hideConsoleLogsInStrictMode,
219
theme,
220
browserTheme,
221
traceUpdatesEnabled,
@@ -216,6 +233,8 @@ function SettingsContextController({
233
setTraceUpdatesEnabled,
234
setShowInlineWarningsAndErrors,
235
showInlineWarningsAndErrors,
236
+ sethideConsoleLogsInStrictMode,
237
+ hideConsoleLogsInStrictMode,
238
theme,
239
browserTheme,
240
traceUpdatesEnabled,
packages/react-devtools-shared/src/devtools/views/Settings/SettingsShared.css
+1
-2
@@ -128,8 +128,7 @@
128
background-color: var(--color-toggle-text);
129
}
130
131
-.ReleaseNotes,
132
-.ConsoleAPIWarning {
131
+.ReleaseNotes {
132
width: 100%;
133
background-color: var(--color-background-hover);
134
padding: 0.25rem 0.5rem;
packages/react-devtools-shared/src/hook.js
+179
-9
@@ -2,11 +2,16 @@
2
* Install the hook on window, which is an event emitter.
3
* Note because Chrome content scripts cannot directly modify the window object,
4
* we are evaling this function by inserting a script tag.
5
- * That's why we have to inline the whole event emitter implementation here.
5
+ * That's why we have to inline the whole event emitter implementation,
6
+ * the string format implementation, and part of the console implementation here.
7
*
8
* @flow
9
*/
10
11
+import type {Fiber} from 'react-reconciler/src/ReactInternalTypes';
12
+import type {ReactRenderer} from './backend/types';
13
+import type {BrowserTheme} from 'react-devtools-shared/src/devtools/views/DevTools';
14
+
15
import {
16
patch as patchConsole,
17
registerRenderer as registerRendererWithConsole,
@@ -152,6 +157,158 @@ export function installHook(target: any): DevToolsHook | null {
157
} catch (err) {}
158
}
159
160
+ // NOTE: KEEP IN SYNC with src/backend/utils.js
161
+ function format(
162
+ maybeMessage: any,
163
+ ...inputArgs: $ReadOnlyArray<any>
164
+ ): string {
165
+ const args = inputArgs.slice();
166
+
167
+ // Symbols cannot be concatenated with Strings.
168
+ let formatted: string =
169
+ typeof maybeMessage === 'symbol'
170
+ ? maybeMessage.toString()
171
+ : '' + maybeMessage;
172
+
173
+ // If the first argument is a string, check for substitutions.
174
+ if (typeof maybeMessage === 'string') {
175
+ if (args.length) {
176
+ const REGEXP = /(%?)(%([jds]))/g;
177
+
178
+ formatted = formatted.replace(REGEXP, (match, escaped, ptn, flag) => {
179
+ let arg = args.shift();
180
+ switch (flag) {
181
+ case 's':
182
+ arg += '';
183
+ break;
184
+ case 'd':
185
+ case 'i':
186
+ arg = parseInt(arg, 10).toString();
187
+ break;
188
+ case 'f':
189
+ arg = parseFloat(arg).toString();
190
+ break;
191
+ }
192
+ if (!escaped) {
193
+ return arg;
194
+ }
195
+ args.unshift(arg);
196
+ return match;
197
+ });
198
+ }
199
+ }
200
+
201
+ // Arguments that remain after formatting.
202
+ if (args.length) {
203
+ for (let i = 0; i < args.length; i++) {
204
+ const arg = args[i];
205
+
206
+ // Symbols cannot be concatenated with Strings.
207
+ formatted += ' ' + (typeof arg === 'symbol' ? arg.toString() : arg);
208
+ }
209
+ }
210
+
211
+ // Update escaped %% values.
212
+ formatted = formatted.replace(/%{2,2}/g, '%');
213
+
214
+ return '' + formatted;
215
+ }
216
+
217
+ // NOTE: KEEP IN SYNC with src/backend/console.js:patch
218
+ function patchConsoleForInitialRenderInExtension(
219
+ renderer: ReactRenderer,
220
+ {
221
+ hideConsoleLogsInStrictMode,
222
+ browserTheme,
223
+ }: {hideConsoleLogsInStrictMode: boolean, browserTheme: BrowserTheme},
224
+ ): void {
225
+ const overrideConsoleMethods = ['error', 'trace', 'warn', 'log'];
226
+
227
+ if (__EXTENSION__) {
228
+ const targetConsole = console;
229
+
230
+ const originalConsoleMethods = {};
231
+
232
+ overrideConsoleMethods.forEach(method => {
233
+ try {
234
+ const originalMethod = (originalConsoleMethods[
235
+ method
236
+ ] = targetConsole[method].__REACT_DEVTOOLS_ORIGINAL_METHOD__
237
+ ? targetConsole[method].__REACT_DEVTOOLS_ORIGINAL_METHOD__
238
+ : targetConsole[method]);
239
+
240
+ const overrideMethod = (...args) => {
241
+ let isInStrictMode = false;
242
+
243
+ // Search for the first renderer that has a current Fiber.
244
+ // We don't handle the edge case of stacks for more than one (e.g. interleaved renderers?)
245
+ const {getCurrentFiber, getIsStrictMode} = renderer;
246
+ if (typeof getCurrentFiber !== 'function') {
247
+ return;
248
+ }
249
+
250
+ const current: ?Fiber = getCurrentFiber();
251
+ if (current != null) {
252
+ try {
253
+ if (
254
+ typeof getIsStrictMode === 'function' &&
255
+ getIsStrictMode()
256
+ ) {
257
+ isInStrictMode = true;
258
+ }
259
+ } catch (error) {
260
+ // Don't let a DevTools or React internal error interfere with logging.
261
+ }
262
+ }
263
+
264
+ if (isInStrictMode) {
265
+ if (!hideConsoleLogsInStrictMode) {
266
+ // Dim the text color of the double logs if we're not
267
+ // hiding them.
268
+ let color;
269
+ switch (method) {
270
+ case 'warn':
271
+ color =
272
+ browserTheme === 'light'
273
+ ? process.env.LIGHT_MODE_DIMMED_WARNING_COLOR
274
+ : process.env.DARK_MODE_DIMMED_WARNING_COLOR;
275
+ break;
276
+ case 'error':
277
+ color =
278
+ browserTheme === 'light'
279
+ ? process.env.LIGHT_MODE_DIMMED_ERROR_COLOR
280
+ : process.env.DARK_MODE_DIMMED_ERROR_COLOR;
281
+ break;
282
+ case 'log':
283
+ default:
284
+ color =
285
+ browserTheme === 'light'
286
+ ? process.env.LIGHT_MODE_DIMMED_LOG_COLOR
287
+ : process.env.DARK_MODE_DIMMED_LOG_COLOR;
288
+ break;
289
+ }
290
+
291
+ if (color) {
292
+ originalMethod(`%c${format(...args)}`, `color: ${color}`);
293
+ } else {
294
+ throw Error('Console color is not defined');
295
+ }
296
+ }
297
+ } else {
298
+ originalMethod(...args);
299
+ }
300
+ };
301
+
302
+ overrideMethod.__REACT_DEVTOOLS_ORIGINAL_METHOD__ = originalMethod;
303
+ originalMethod.__REACT_DEVTOOLS_OVERRIDE_METHOD__ = overrideMethod;
304
+
305
+ // $FlowFixMe property error|warn is not writable.
306
+ targetConsole[method] = overrideMethod;
307
+ } catch (error) {}
308
+ });
309
+ }
310
+ }
311
+
312
let uidCounter = 0;
313
314
function inject(renderer) {
@@ -164,19 +321,22 @@ export function installHook(target: any): DevToolsHook | null {
321
322
// Patching the console enables DevTools to do a few useful things:
323
// * Append component stacks to warnings and error messages
324
+ // * Disabling or marking logs during a double render in Strict Mode
325
// * Disable logging during re-renders to inspect hooks (see inspectHooksOfFiber)
326
//
327
// For React Native, we intentionally patch early (during injection).
328
// This provides React Native developers with components stacks even if they don't run DevTools.
329
+ //
330
// This won't work for DOM though, since this entire file is eval'ed and inserted as a script tag.
172
- // In that case, we'll patch later (when the frontend attaches).
331
+ // In that case, we'll only patch parts of the console that are needed during the first render
332
+ // and patch everything else later (when the frontend attaches).
333
//
334
// Don't patch in test environments because we don't want to interfere with Jest's own console overrides.
335
//
336
// Note that because this function is inlined, this conditional check must only use static booleans.
337
// Otherwise the extension will throw with an undefined error.
338
// (See comments in the try/catch below for more context on inlining.)
179
- if (!__EXTENSION__ && !__TEST__) {
339
+ if (!__TEST__) {
340
try {
341
const appendComponentStack =
342
window.__REACT_DEVTOOLS_APPEND_COMPONENT_STACK__ !== false;
@@ -184,6 +344,9 @@ export function installHook(target: any): DevToolsHook | null {
344
window.__REACT_DEVTOOLS_BREAK_ON_CONSOLE_ERRORS__ === true;
345
const showInlineWarningsAndErrors =
346
window.__REACT_DEVTOOLS_SHOW_INLINE_WARNINGS_AND_ERRORS__ !== false;
347
+ const hideConsoleLogsInStrictMode =
348
+ window.__REACT_DEVTOOLS_HIDE_CONSOLE_LOGS_IN_STRICT_MODE__ === true;
349
+ const browserTheme = window.__REACT_DEVTOOLS_BROWSER_THEME__;
350
351
// The installHook() function is injected by being stringified in the browser,
352
// so imports outside of this function do not get included.
@@ -192,16 +355,19 @@ export function installHook(target: any): DevToolsHook | null {
355
// but Webpack wraps imports with an object (e.g. _backend_console__WEBPACK_IMPORTED_MODULE_0__)
356
// and the object itself will be undefined as well for the reasons mentioned above,
357
// so we use try/catch instead.
195
- if (
196
- appendComponentStack ||
197
- breakOnConsoleErrors ||
198
- showInlineWarningsAndErrors
199
- ) {
358
+ if (!__EXTENSION__) {
359
registerRendererWithConsole(renderer);
360
patchConsole({
361
appendComponentStack,
362
breakOnConsoleErrors,
363
showInlineWarningsAndErrors,
364
+ hideConsoleLogsInStrictMode,
365
+ browserTheme,
366
+ });
367
+ } else {
368
+ patchConsoleForInitialRenderInExtension(renderer, {
369
+ hideConsoleLogsInStrictMode,
370
+ browserTheme,
371
});
372
}
373
} catch (error) {}
@@ -215,7 +381,11 @@ export function installHook(target: any): DevToolsHook | null {
381
hook.rendererInterfaces.set(id, rendererInterface);
382
}
383
218
- hook.emit('renderer', {id, renderer, reactBuildType});
384
+ hook.emit('renderer', {
385
+ id,
386
+ renderer,
387
+ reactBuildType,
388
+ });
389
390
return id;
391
}
packages/react-devtools-shared/src/utils.js
+20
@@ -37,6 +37,7 @@ import {
37
LOCAL_STORAGE_SHOULD_BREAK_ON_CONSOLE_ERRORS,
38
LOCAL_STORAGE_SHOULD_PATCH_CONSOLE_KEY,
39
LOCAL_STORAGE_SHOW_INLINE_WARNINGS_AND_ERRORS_KEY,
40
+ LOCAL_STORAGE_HIDE_CONSOLE_LOGS_IN_STRICT_MODE,
41
} from './constants';
42
import {ComponentFilterElementType, ElementTypeHostComponent} from './types';
43
import {
@@ -317,6 +318,25 @@ export function setBreakOnConsoleErrors(value: boolean): void {
318
);
319
}
320
321
+export function getHideConsoleLogsInStrictMode(): boolean {
322
+ try {
323
+ const raw = localStorageGetItem(
324
+ LOCAL_STORAGE_HIDE_CONSOLE_LOGS_IN_STRICT_MODE,
325
+ );
326
+ if (raw != null) {
327
+ return JSON.parse(raw);
328
+ }
329
+ } catch (error) {}
330
+ return false;
331
+}
332
+
333
+export function sethideConsoleLogsInStrictMode(value: boolean): void {
334
+ localStorageSetItem(
335
+ LOCAL_STORAGE_HIDE_CONSOLE_LOGS_IN_STRICT_MODE,
336
+ JSON.stringify(value),
337
+ );
338
+}
339
+
340
export function getShowInlineWarningsAndErrors(): boolean {
341
try {
342
const raw = localStorageGetItem(
packages/react-devtools-shell/src/app/console.js
+4
@@ -37,3 +37,7 @@ export function ignoreErrors(errorsToIgnore: Array<string>): void {
37
export function ignoreWarnings(warningsToIgnore: Array<string>): void {
38
ignoreStrings('warn', warningsToIgnore);
39
}
40
+
41
+export function ignoreLogs(logsToIgnore: Array<string>): void {
42
+ ignoreStrings('log', logsToIgnore);
43
+}
packages/react-devtools-shell/src/app/index.js
+2
-1
@@ -19,7 +19,7 @@ import ToDoList from './ToDoList';
19
import Toggle from './Toggle';
20
import ErrorBoundaries from './ErrorBoundaries';
21
import SuspenseTree from './SuspenseTree';
22
-import {ignoreErrors, ignoreWarnings} from './console';
22
+import {ignoreErrors, ignoreLogs, ignoreWarnings} from './console';
23
24
import './styles.css';
25
@@ -32,6 +32,7 @@ ignoreErrors([
32
'Warning: %s is deprecated in StrictMode.', // findDOMNode
33
]);
34
ignoreWarnings(['Warning: componentWillReceiveProps has been renamed']);
35
+ignoreLogs([]);
36
37
const roots = [];
38
packages/react-devtools-shell/src/devtools.js
+8
-6
@@ -19,6 +19,14 @@ contentWindow.__REACT_DEVTOOLS_TARGET_WINDOW__ = window;
19
20
initializeBackend(contentWindow);
21
22
+// Initialize the front end and activate the backend early so that we are able
23
+// to pass console settings in local storage to the backend before initial render
24
+const DevTools = initializeFrontend(contentWindow);
25
+
26
+// Activate the backend only once the DevTools frontend Store has been initialized.
27
+// Otherwise the Store may miss important initial tree op codes.
28
+activateBackend(contentWindow);
29
+
30
const container = ((document.getElementById('devtools'): any): HTMLElement);
31
32
let isTestAppMounted = true;
@@ -45,12 +53,6 @@ mountButton.addEventListener('click', function() {
53
inject('dist/app.js', () => {
54
initDevTools({
55
connect(cb) {
48
- const DevTools = initializeFrontend(contentWindow);
49
-
50
- // Activate the backend only once the DevTools frontend Store has been initialized.
51
- // Otherwise the Store may miss important initial tree op codes.
52
- activateBackend(contentWindow);
53
-
56
const root = createRoot(container);
57
root.render(
58
createElement(DevTools, {
packages/react-devtools-shell/webpack.config.js
+12
@@ -1,6 +1,12 @@
1
const {resolve} = require('path');
2
const {DefinePlugin} = require('webpack');
3
const {
4
+ DARK_MODE_DIMMED_WARNING_COLOR,
5
+ DARK_MODE_DIMMED_ERROR_COLOR,
6
+ DARK_MODE_DIMMED_LOG_COLOR,
7
+ LIGHT_MODE_DIMMED_WARNING_COLOR,
8
+ LIGHT_MODE_DIMMED_ERROR_COLOR,
9
+ LIGHT_MODE_DIMMED_LOG_COLOR,
10
GITHUB_URL,
11
getVersionString,
12
} = require('react-devtools-extensions/utils');
@@ -59,6 +65,12 @@ const config = {
65
'process.env.GITHUB_URL': `"${GITHUB_URL}"`,
66
'process.env.DEVTOOLS_PACKAGE': `"react-devtools-shell"`,
67
'process.env.DEVTOOLS_VERSION': `"${DEVTOOLS_VERSION}"`,
68
+ 'process.env.DARK_MODE_DIMMED_WARNING_COLOR': `"${DARK_MODE_DIMMED_WARNING_COLOR}"`,
69
+ 'process.env.DARK_MODE_DIMMED_ERROR_COLOR': `"${DARK_MODE_DIMMED_ERROR_COLOR}"`,
70
+ 'process.env.DARK_MODE_DIMMED_LOG_COLOR': `"${DARK_MODE_DIMMED_LOG_COLOR}"`,
71
+ 'process.env.LIGHT_MODE_DIMMED_WARNING_COLOR': `"${LIGHT_MODE_DIMMED_WARNING_COLOR}"`,
72
+ 'process.env.LIGHT_MODE_DIMMED_ERROR_COLOR': `"${LIGHT_MODE_DIMMED_ERROR_COLOR}"`,
73
+ 'process.env.LIGHT_MODE_DIMMED_LOG_COLOR': `"${LIGHT_MODE_DIMMED_LOG_COLOR}"`,
74
}),
75
],
76
module: {
packages/react-reconciler/src/ReactFiberBeginWork.new.js
+13
-11
@@ -151,7 +151,11 @@ import {
151
getOffscreenContainerProps,
152
} from './ReactFiberHostConfig';
153
import type {SuspenseInstance} from './ReactFiberHostConfig';
154
-import {shouldError, shouldSuspend} from './ReactFiberReconciler';
154
+import {
155
+ shouldError,
156
+ shouldSuspend,
157
+ setIsStrictModeForDevtools,
158
+} from './ReactFiberReconciler';
159
import {pushHostContext, pushHostContainer} from './ReactFiberHostContext.new';
160
import {
161
suspenseStackCursor,
@@ -235,8 +239,6 @@ import {createClassErrorUpdate} from './ReactFiberThrow.new';
239
import {completeSuspendedOffscreenHostContainer} from './ReactFiberCompleteWork.new';
240
import is from 'shared/objectIs';
241
238
-import {disableLogs, reenableLogs} from 'shared/ConsolePatchingDev';
239
-
242
const ReactCurrentOwner = ReactSharedInternals.ReactCurrentOwner;
243
244
let didReceiveUpdate: boolean = false;
@@ -379,7 +381,7 @@ function updateForwardRef(
381
debugRenderPhaseSideEffectsForStrictMode &&
382
workInProgress.mode & StrictLegacyMode
383
) {
382
- disableLogs();
384
+ setIsStrictModeForDevtools(true);
385
try {
386
nextChildren = renderWithHooks(
387
current,
@@ -390,7 +392,7 @@ function updateForwardRef(
392
renderLanes,
393
);
394
} finally {
393
- reenableLogs();
395
+ setIsStrictModeForDevtools(false);
396
}
397
}
398
setIsRendering(false);
@@ -984,7 +986,7 @@ function updateFunctionComponent(
986
debugRenderPhaseSideEffectsForStrictMode &&
987
workInProgress.mode & StrictLegacyMode
988
) {
987
- disableLogs();
989
+ setIsStrictModeForDevtools(true);
990
try {
991
nextChildren = renderWithHooks(
992
current,
@@ -995,7 +997,7 @@ function updateFunctionComponent(
997
renderLanes,
998
);
999
} finally {
998
- reenableLogs();
1000
+ setIsStrictModeForDevtools(false);
1001
}
1002
}
1003
setIsRendering(false);
@@ -1201,11 +1203,11 @@ function finishClassComponent(
1203
debugRenderPhaseSideEffectsForStrictMode &&
1204
workInProgress.mode & StrictLegacyMode
1205
) {
1204
- disableLogs();
1206
+ setIsStrictModeForDevtools(true);
1207
try {
1208
instance.render();
1209
} finally {
1208
- reenableLogs();
1210
+ setIsStrictModeForDevtools(false);
1211
}
1212
}
1213
setIsRendering(false);
@@ -1741,7 +1743,7 @@ function mountIndeterminateComponent(
1743
debugRenderPhaseSideEffectsForStrictMode &&
1744
workInProgress.mode & StrictLegacyMode
1745
) {
1744
- disableLogs();
1746
+ setIsStrictModeForDevtools(true);
1747
try {
1748
value = renderWithHooks(
1749
null,
@@ -1752,7 +1754,7 @@ function mountIndeterminateComponent(
1754
renderLanes,
1755
);
1756
} finally {
1755
- reenableLogs();
1757
+ setIsStrictModeForDevtools(false);
1758
}
1759
}
1760
}
packages/react-reconciler/src/ReactFiberBeginWork.old.js
+13
-11
@@ -151,7 +151,11 @@ import {
151
getOffscreenContainerProps,
152
} from './ReactFiberHostConfig';
153
import type {SuspenseInstance} from './ReactFiberHostConfig';
154
-import {shouldError, shouldSuspend} from './ReactFiberReconciler';
154
+import {
155
+ shouldError,
156
+ shouldSuspend,
157
+ setIsStrictModeForDevtools,
158
+} from './ReactFiberReconciler';
159
import {pushHostContext, pushHostContainer} from './ReactFiberHostContext.old';
160
import {
161
suspenseStackCursor,
@@ -235,8 +239,6 @@ import {createClassErrorUpdate} from './ReactFiberThrow.old';
239
import {completeSuspendedOffscreenHostContainer} from './ReactFiberCompleteWork.old';
240
import is from 'shared/objectIs';
241
238
-import {disableLogs, reenableLogs} from 'shared/ConsolePatchingDev';
239
-
242
const ReactCurrentOwner = ReactSharedInternals.ReactCurrentOwner;
243
244
let didReceiveUpdate: boolean = false;
@@ -379,7 +381,7 @@ function updateForwardRef(
381
debugRenderPhaseSideEffectsForStrictMode &&
382
workInProgress.mode & StrictLegacyMode
383
) {
382
- disableLogs();
384
+ setIsStrictModeForDevtools(true);
385
try {
386
nextChildren = renderWithHooks(
387
current,
@@ -390,7 +392,7 @@ function updateForwardRef(
392
renderLanes,
393
);
394
} finally {
393
- reenableLogs();
395
+ setIsStrictModeForDevtools(false);
396
}
397
}
398
setIsRendering(false);
@@ -984,7 +986,7 @@ function updateFunctionComponent(
986
debugRenderPhaseSideEffectsForStrictMode &&
987
workInProgress.mode & StrictLegacyMode
988
) {
987
- disableLogs();
989
+ setIsStrictModeForDevtools(true);
990
try {
991
nextChildren = renderWithHooks(
992
current,
@@ -995,7 +997,7 @@ function updateFunctionComponent(
997
renderLanes,
998
);
999
} finally {
998
- reenableLogs();
1000
+ setIsStrictModeForDevtools(false);
1001
}
1002
}
1003
setIsRendering(false);
@@ -1201,11 +1203,11 @@ function finishClassComponent(
1203
debugRenderPhaseSideEffectsForStrictMode &&
1204
workInProgress.mode & StrictLegacyMode
1205
) {
1204
- disableLogs();
1206
+ setIsStrictModeForDevtools(true);
1207
try {
1208
instance.render();
1209
} finally {
1208
- reenableLogs();
1210
+ setIsStrictModeForDevtools(false);
1211
}
1212
}
1213
setIsRendering(false);
@@ -1741,7 +1743,7 @@ function mountIndeterminateComponent(
1743
debugRenderPhaseSideEffectsForStrictMode &&
1744
workInProgress.mode & StrictLegacyMode
1745
) {
1744
- disableLogs();
1746
+ setIsStrictModeForDevtools(true);
1747
try {
1748
value = renderWithHooks(
1749
null,
@@ -1752,7 +1754,7 @@ function mountIndeterminateComponent(
1754
renderLanes,
1755
);
1756
} finally {
1755
- reenableLogs();
1757
+ setIsStrictModeForDevtools(false);
1758
}
1759
}
1760
}
packages/react-reconciler/src/ReactFiberClassComponent.new.js
+7
-7
@@ -38,6 +38,7 @@ import getComponentNameFromType from 'shared/getComponentNameFromType';
38
import invariant from 'shared/invariant';
39
import isArray from 'shared/isArray';
40
import {REACT_CONTEXT_TYPE, REACT_PROVIDER_TYPE} from 'shared/ReactSymbols';
41
+import {setIsStrictModeForDevtools} from './ReactFiberReconciler';
42
43
import {resolveDefaultProps} from './ReactFiberLazyComponent.new';
44
import {
@@ -75,7 +76,6 @@ import {
76
} from './ReactFiberWorkLoop.new';
77
import {logForceUpdateScheduled, logStateUpdateScheduled} from './DebugTracing';
78
78
-import {disableLogs, reenableLogs} from 'shared/ConsolePatchingDev';
79
import {
80
markForceUpdateScheduled,
81
markStateUpdateScheduled,
@@ -175,12 +175,12 @@ function applyDerivedStateFromProps(
175
debugRenderPhaseSideEffectsForStrictMode &&
176
workInProgress.mode & StrictLegacyMode
177
) {
178
- disableLogs();
178
+ setIsStrictModeForDevtools(true);
179
try {
180
// Invoke the function an extra time to help detect side-effects.
181
partialState = getDerivedStateFromProps(nextProps, prevState);
182
} finally {
183
- reenableLogs();
183
+ setIsStrictModeForDevtools(false);
184
}
185
}
186
warnOnUndefinedDerivedState(ctor, partialState);
@@ -328,7 +328,7 @@ function checkShouldComponentUpdate(
328
debugRenderPhaseSideEffectsForStrictMode &&
329
workInProgress.mode & StrictLegacyMode
330
) {
331
- disableLogs();
331
+ setIsStrictModeForDevtools(true);
332
try {
333
// Invoke the function an extra time to help detect side-effects.
334
shouldUpdate = instance.shouldComponentUpdate(
@@ -337,7 +337,7 @@ function checkShouldComponentUpdate(
337
nextContext,
338
);
339
} finally {
340
- reenableLogs();
340
+ setIsStrictModeForDevtools(false);
341
}
342
}
343
if (shouldUpdate === undefined) {
@@ -662,11 +662,11 @@ function constructClassInstance(
662
debugRenderPhaseSideEffectsForStrictMode &&
663
workInProgress.mode & StrictLegacyMode
664
) {
665
- disableLogs();
665
+ setIsStrictModeForDevtools(true);
666
try {
667
instance = new ctor(props, context); // eslint-disable-line no-new
668
} finally {
669
- reenableLogs();
669
+ setIsStrictModeForDevtools(false);
670
}
671
}
672
}
packages/react-reconciler/src/ReactFiberClassComponent.old.js
+7
-7
@@ -38,6 +38,7 @@ import getComponentNameFromType from 'shared/getComponentNameFromType';
38
import invariant from 'shared/invariant';
39
import isArray from 'shared/isArray';
40
import {REACT_CONTEXT_TYPE, REACT_PROVIDER_TYPE} from 'shared/ReactSymbols';
41
+import {setIsStrictModeForDevtools} from './ReactFiberReconciler';
42
43
import {resolveDefaultProps} from './ReactFiberLazyComponent.old';
44
import {
@@ -75,7 +76,6 @@ import {
76
} from './ReactFiberWorkLoop.old';
77
import {logForceUpdateScheduled, logStateUpdateScheduled} from './DebugTracing';
78
78
-import {disableLogs, reenableLogs} from 'shared/ConsolePatchingDev';
79
import {
80
markForceUpdateScheduled,
81
markStateUpdateScheduled,
@@ -175,12 +175,12 @@ function applyDerivedStateFromProps(
175
debugRenderPhaseSideEffectsForStrictMode &&
176
workInProgress.mode & StrictLegacyMode
177
) {
178
- disableLogs();
178
+ setIsStrictModeForDevtools(true);
179
try {
180
// Invoke the function an extra time to help detect side-effects.
181
partialState = getDerivedStateFromProps(nextProps, prevState);
182
} finally {
183
- reenableLogs();
183
+ setIsStrictModeForDevtools(false);
184
}
185
}
186
warnOnUndefinedDerivedState(ctor, partialState);
@@ -328,7 +328,7 @@ function checkShouldComponentUpdate(
328
debugRenderPhaseSideEffectsForStrictMode &&
329
workInProgress.mode & StrictLegacyMode
330
) {
331
- disableLogs();
331
+ setIsStrictModeForDevtools(true);
332
try {
333
// Invoke the function an extra time to help detect side-effects.
334
shouldUpdate = instance.shouldComponentUpdate(
@@ -337,7 +337,7 @@ function checkShouldComponentUpdate(
337
nextContext,
338
);
339
} finally {
340
- reenableLogs();
340
+ setIsStrictModeForDevtools(false);
341
}
342
}
343
if (shouldUpdate === undefined) {
@@ -662,11 +662,11 @@ function constructClassInstance(
662
debugRenderPhaseSideEffectsForStrictMode &&
663
workInProgress.mode & StrictLegacyMode
664
) {
665
- disableLogs();
665
+ setIsStrictModeForDevtools(true);
666
try {
667
instance = new ctor(props, context); // eslint-disable-line no-new
668
} finally {
669
- reenableLogs();
669
+ setIsStrictModeForDevtools(false);
670
}
671
}
672
}
packages/react-reconciler/src/ReactFiberHooks.new.js
+4
-3
@@ -113,6 +113,7 @@ import {
113
entangleTransitions,
114
} from './ReactUpdateQueue.new';
115
import {pushInterleavedQueue} from './ReactFiberInterleavedUpdates.new';
116
+import {getIsStrictModeForDevtools} from './ReactFiberReconciler.new';
117
118
const {ReactCurrentDispatcher, ReactCurrentBatchConfig} = ReactSharedInternals;
119
@@ -1021,9 +1022,9 @@ function readFromUnsubscribedMutableSource<Source, Snapshot>(
1022
// dev, we can present a more accurate error message.
1023
if (__DEV__) {
1024
// eslint-disable-next-line react-internal/no-production-logging
1024
- if (console.log.__reactDisabledLog) {
1025
- // If the logs are disabled, this is the dev-only double render. This is
1026
- // only reachable if there was a mutation during render. Show a helpful
1025
+ if (getIsStrictModeForDevtools()) {
1026
+ // If getIsStrictModeForDevtools is true, this is the dev-only double render
1027
+ // This is only reachable if there was a mutation during render. Show a helpful
1028
// error message.
1029
//
1030
// Something interesting to note: because we only double render in
packages/react-reconciler/src/ReactFiberHooks.old.js
+4
-3
@@ -113,6 +113,7 @@ import {
113
entangleTransitions,
114
} from './ReactUpdateQueue.old';
115
import {pushInterleavedQueue} from './ReactFiberInterleavedUpdates.old';
116
+import {getIsStrictModeForDevtools} from './ReactFiberReconciler.old';
117
118
const {ReactCurrentDispatcher, ReactCurrentBatchConfig} = ReactSharedInternals;
119
@@ -1021,9 +1022,9 @@ function readFromUnsubscribedMutableSource<Source, Snapshot>(
1022
// dev, we can present a more accurate error message.
1023
if (__DEV__) {
1024
// eslint-disable-next-line react-internal/no-production-logging
1024
- if (console.log.__reactDisabledLog) {
1025
- // If the logs are disabled, this is the dev-only double render. This is
1026
- // only reachable if there was a mutation during render. Show a helpful
1025
+ if (getIsStrictModeForDevtools()) {
1026
+ // If getIsStrictModeForDevtools is true, this is the dev-only double render
1027
+ // This is only reachable if there was a mutation during render. Show a helpful
1028
// error message.
1029
//
1030
// Something interesting to note: because we only double render in
packages/react-reconciler/src/ReactFiberReconciler.js
+11
@@ -49,6 +49,8 @@ import {
49
registerMutableSourceForHydration as registerMutableSourceForHydration_old,
50
runWithPriority as runWithPriority_old,
51
getCurrentUpdatePriority as getCurrentUpdatePriority_old,
52
+ getIsStrictModeForDevtools as getIsStrictModeForDevtools_old,
53
+ setIsStrictModeForDevtools as setIsStrictModeForDevtools_old,
54
} from './ReactFiberReconciler.old';
55
56
import {
@@ -86,6 +88,8 @@ import {
88
registerMutableSourceForHydration as registerMutableSourceForHydration_new,
89
runWithPriority as runWithPriority_new,
90
getCurrentUpdatePriority as getCurrentUpdatePriority_new,
91
+ getIsStrictModeForDevtools as getIsStrictModeForDevtools_new,
92
+ setIsStrictModeForDevtools as setIsStrictModeForDevtools_new,
93
} from './ReactFiberReconciler.new';
94
95
export const createContainer = enableNewReconciler
@@ -190,3 +194,10 @@ export const registerMutableSourceForHydration = enableNewReconciler
194
export const runWithPriority = enableNewReconciler
195
? runWithPriority_new
196
: runWithPriority_old;
197
+
198
+export const getIsStrictModeForDevtools = enableNewReconciler
199
+ ? getIsStrictModeForDevtools_new
200
+ : getIsStrictModeForDevtools_old;
201
+export const setIsStrictModeForDevtools = enableNewReconciler
202
+ ? setIsStrictModeForDevtools_new
203
+ : setIsStrictModeForDevtools_old;
packages/react-reconciler/src/ReactFiberReconciler.new.js
+21
@@ -105,6 +105,9 @@ export {
105
observeVisibleRects,
106
} from './ReactTestSelectors';
107
108
+import * as Scheduler from './Scheduler';
109
+import {setSuppressWarning} from 'shared/consoleWithStackDev';
110
+
111
type OpaqueRoot = FiberRoot;
112
113
// 0 is PROD, 1 is DEV.
@@ -458,6 +461,8 @@ export function shouldSuspend(fiber: Fiber): boolean {
461
return shouldSuspendImpl(fiber);
462
}
463
464
+let isStrictMode = false;
465
+
466
let overrideHookState = null;
467
let overrideHookStateDeletePath = null;
468
let overrideHookStateRenamePath = null;
@@ -707,6 +712,21 @@ function getCurrentFiberForDevTools() {
712
return ReactCurrentFiberCurrent;
713
}
714
715
+export function getIsStrictModeForDevtools() {
716
+ return isStrictMode;
717
+}
718
+
719
+export function setIsStrictModeForDevtools(newIsStrictMode: boolean) {
720
+ // We're in a test because Scheduler.unstable_yieldValue only exists
721
+ // in SchedulerMock. To reduce the noise in strict mode tests,
722
+ // suppress warnings and disable scheduler yielding during the double render
723
+ if (typeof Scheduler.unstable_yieldValue === 'function') {
724
+ Scheduler.unstable_setDisableYieldValue(newIsStrictMode);
725
+ setSuppressWarning(newIsStrictMode);
726
+ }
727
+ isStrictMode = newIsStrictMode;
728
+}
729
+
730
export function injectIntoDevTools(devToolsConfig: DevToolsConfig): boolean {
731
const {findFiberByHostInstance} = devToolsConfig;
732
const {ReactCurrentDispatcher} = ReactSharedInternals;
@@ -736,6 +756,7 @@ export function injectIntoDevTools(devToolsConfig: DevToolsConfig): boolean {
756
setRefreshHandler: __DEV__ ? setRefreshHandler : null,
757
// Enables DevTools to append owner stacks to error messages in DEV mode.
758
getCurrentFiber: __DEV__ ? getCurrentFiberForDevTools : null,
759
+ getIsStrictMode: __DEV__ ? getIsStrictModeForDevtools : null,
760
// Enables DevTools to detect reconciler version rather than renderer version
761
// which may not match for third party renderers.
762
reconcilerVersion: ReactVersion,
packages/react-reconciler/src/ReactFiberReconciler.old.js
+21
@@ -105,6 +105,9 @@ export {
105
observeVisibleRects,
106
} from './ReactTestSelectors';
107
108
+import * as Scheduler from './Scheduler';
109
+import {setSuppressWarning} from 'shared/consoleWithStackDev';
110
+
111
type OpaqueRoot = FiberRoot;
112
113
// 0 is PROD, 1 is DEV.
@@ -458,6 +461,8 @@ export function shouldSuspend(fiber: Fiber): boolean {
461
return shouldSuspendImpl(fiber);
462
}
463
464
+let isStrictMode = false;
465
+
466
let overrideHookState = null;
467
let overrideHookStateDeletePath = null;
468
let overrideHookStateRenamePath = null;
@@ -707,6 +712,21 @@ function getCurrentFiberForDevTools() {
712
return ReactCurrentFiberCurrent;
713
}
714
715
+export function getIsStrictModeForDevtools() {
716
+ return isStrictMode;
717
+}
718
+
719
+export function setIsStrictModeForDevtools(newIsStrictMode: boolean) {
720
+ // We're in a test because Scheduler.unstable_yieldValue only exists
721
+ // in SchedulerMock. To reduce the noise in strict mode tests,
722
+ // suppress warnings and disable scheduler yielding during the double render
723
+ if (typeof Scheduler.unstable_yieldValue === 'function') {
724
+ Scheduler.unstable_setDisableYieldValue(newIsStrictMode);
725
+ setSuppressWarning(newIsStrictMode);
726
+ }
727
+ isStrictMode = newIsStrictMode;
728
+}
729
+
730
export function injectIntoDevTools(devToolsConfig: DevToolsConfig): boolean {
731
const {findFiberByHostInstance} = devToolsConfig;
732
const {ReactCurrentDispatcher} = ReactSharedInternals;
@@ -736,6 +756,7 @@ export function injectIntoDevTools(devToolsConfig: DevToolsConfig): boolean {
756
setRefreshHandler: __DEV__ ? setRefreshHandler : null,
757
// Enables DevTools to append owner stacks to error messages in DEV mode.
758
getCurrentFiber: __DEV__ ? getCurrentFiberForDevTools : null,
759
+ getIsStrictMode: __DEV__ ? getIsStrictModeForDevtools : null,
760
// Enables DevTools to detect reconciler version rather than renderer version
761
// which may not match for third party renderers.
762
reconcilerVersion: ReactVersion,
packages/react-reconciler/src/ReactUpdateQueue.new.js
+5
-6
@@ -110,11 +110,10 @@ import {
110
isInterleavedUpdate,
111
} from './ReactFiberWorkLoop.new';
112
import {pushInterleavedQueue} from './ReactFiberInterleavedUpdates.new';
113
+import {setIsStrictModeForDevtools} from './ReactFiberReconciler';
114
115
import invariant from 'shared/invariant';
116
116
-import {disableLogs, reenableLogs} from 'shared/ConsolePatchingDev';
117
-
117
export type Update<State> = {|
118
// TODO: Temporary field. Will remove this by storing a map of
119
// transition -> event time on the root.
@@ -394,11 +393,11 @@ function getStateFromUpdate<State>(
393
debugRenderPhaseSideEffectsForStrictMode &&
394
workInProgress.mode & StrictLegacyMode
395
) {
397
- disableLogs();
396
+ setIsStrictModeForDevtools(true);
397
try {
398
payload.call(instance, prevState, nextProps);
399
} finally {
401
- reenableLogs();
400
+ setIsStrictModeForDevtools(false);
401
}
402
}
403
exitDisallowedContextReadInDEV();
@@ -427,11 +426,11 @@ function getStateFromUpdate<State>(
426
debugRenderPhaseSideEffectsForStrictMode &&
427
workInProgress.mode & StrictLegacyMode
428
) {
430
- disableLogs();
429
+ setIsStrictModeForDevtools(true);
430
try {
431
payload.call(instance, prevState, nextProps);
432
} finally {
434
- reenableLogs();
433
+ setIsStrictModeForDevtools(false);
434
}
435
}
436
exitDisallowedContextReadInDEV();
packages/react-reconciler/src/ReactUpdateQueue.old.js
+5
-6
@@ -110,11 +110,10 @@ import {
110
isInterleavedUpdate,
111
} from './ReactFiberWorkLoop.old';
112
import {pushInterleavedQueue} from './ReactFiberInterleavedUpdates.old';
113
+import {setIsStrictModeForDevtools} from './ReactFiberReconciler';
114
115
import invariant from 'shared/invariant';
116
116
-import {disableLogs, reenableLogs} from 'shared/ConsolePatchingDev';
117
-
117
export type Update<State> = {|
118
// TODO: Temporary field. Will remove this by storing a map of
119
// transition -> event time on the root.
@@ -394,11 +393,11 @@ function getStateFromUpdate<State>(
393
debugRenderPhaseSideEffectsForStrictMode &&
394
workInProgress.mode & StrictLegacyMode
395
) {
397
- disableLogs();
396
+ setIsStrictModeForDevtools(true);
397
try {
398
payload.call(instance, prevState, nextProps);
399
} finally {
401
- reenableLogs();
400
+ setIsStrictModeForDevtools(false);
401
}
402
}
403
exitDisallowedContextReadInDEV();
@@ -427,11 +426,11 @@ function getStateFromUpdate<State>(
426
debugRenderPhaseSideEffectsForStrictMode &&
427
workInProgress.mode & StrictLegacyMode
428
) {
430
- disableLogs();
429
+ setIsStrictModeForDevtools(true);
430
try {
431
payload.call(instance, prevState, nextProps);
432
} finally {
434
- reenableLogs();
433
+ setIsStrictModeForDevtools(false);
434
}
435
}
436
exitDisallowedContextReadInDEV();
packages/react-reconciler/src/Scheduler.js
+6
@@ -26,3 +26,9 @@ export const NormalPriority = Scheduler.unstable_NormalPriority;
26
export const LowPriority = Scheduler.unstable_LowPriority;
27
export const IdlePriority = Scheduler.unstable_IdlePriority;
28
export type SchedulerCallback = (isSync: boolean) => SchedulerCallback | null;
29
+
30
+// this doesn't actually exist on the scheduler, but it *does*
31
+// on scheduler/unstable_mock, which we'll need for internal testing
32
+export const unstable_yieldValue = Scheduler.unstable_yieldValue;
33
+export const unstable_setDisableYieldValue =
34
+ Scheduler.unstable_setDisableYieldValue;
packages/react/src/__tests__/ReactStrictMode-test.js
+13
-13
@@ -893,8 +893,8 @@ describe('context legacy', () => {
893
ReactDOM.render(<Root />, container);
894
});
895
896
- describe('disableLogs', () => {
897
- it('disables logs once for class double render', () => {
896
+ describe('logging', () => {
897
+ it('does not disable logs for class double render', () => {
898
spyOnDevAndProd(console, 'log');
899
900
let count = 0;
@@ -915,14 +915,14 @@ describe('context legacy', () => {
915
);
916
917
expect(count).toBe(__DEV__ ? 2 : 1);
918
- expect(console.log).toBeCalledTimes(1);
918
+ expect(console.log).toBeCalledTimes(__DEV__ ? 2 : 1);
919
// Note: we should display the first log because otherwise
920
// there is a risk of suppressing warnings when they happen,
921
// and on the next render they'd get deduplicated and ignored.
922
expect(console.log).toBeCalledWith('foo 1');
923
});
924
925
- it('disables logs once for class double ctor', () => {
925
+ it('does not disable logs for class double ctor', () => {
926
spyOnDevAndProd(console, 'log');
927
928
let count = 0;
@@ -946,14 +946,14 @@ describe('context legacy', () => {
946
);
947
948
expect(count).toBe(__DEV__ ? 2 : 1);
949
- expect(console.log).toBeCalledTimes(1);
949
+ expect(console.log).toBeCalledTimes(__DEV__ ? 2 : 1);
950
// Note: we should display the first log because otherwise
951
// there is a risk of suppressing warnings when they happen,
952
// and on the next render they'd get deduplicated and ignored.
953
expect(console.log).toBeCalledWith('foo 1');
954
});
955
956
- it('disables logs once for class double getDerivedStateFromProps', () => {
956
+ it('does not disable logs for class double getDerivedStateFromProps', () => {
957
spyOnDevAndProd(console, 'log');
958
959
let count = 0;
@@ -978,14 +978,14 @@ describe('context legacy', () => {
978
);
979
980
expect(count).toBe(__DEV__ ? 2 : 1);
981
- expect(console.log).toBeCalledTimes(1);
981
+ expect(console.log).toBeCalledTimes(__DEV__ ? 2 : 1);
982
// Note: we should display the first log because otherwise
983
// there is a risk of suppressing warnings when they happen,
984
// and on the next render they'd get deduplicated and ignored.
985
expect(console.log).toBeCalledWith('foo 1');
986
});
987
988
- it('disables logs once for class double shouldComponentUpdate', () => {
988
+ it('does not disable logs for class double shouldComponentUpdate', () => {
989
spyOnDevAndProd(console, 'log');
990
991
let count = 0;
@@ -1017,14 +1017,14 @@ describe('context legacy', () => {
1017
);
1018
1019
expect(count).toBe(__DEV__ ? 2 : 1);
1020
- expect(console.log).toBeCalledTimes(1);
1020
+ expect(console.log).toBeCalledTimes(__DEV__ ? 2 : 1);
1021
// Note: we should display the first log because otherwise
1022
// there is a risk of suppressing warnings when they happen,
1023
// and on the next render they'd get deduplicated and ignored.
1024
expect(console.log).toBeCalledWith('foo 1');
1025
});
1026
1027
- it('disables logs once for class state updaters', () => {
1027
+ it('does not disable logs for class state updaters', () => {
1028
spyOnDevAndProd(console, 'log');
1029
1030
let inst;
@@ -1051,14 +1051,14 @@ describe('context legacy', () => {
1051
});
1052
1053
expect(count).toBe(__DEV__ ? 2 : 1);
1054
- expect(console.log).toBeCalledTimes(1);
1054
+ expect(console.log).toBeCalledTimes(__DEV__ ? 2 : 1);
1055
// Note: we should display the first log because otherwise
1056
// there is a risk of suppressing warnings when they happen,
1057
// and on the next render they'd get deduplicated and ignored.
1058
expect(console.log).toBeCalledWith('foo 1');
1059
});
1060
1061
- it('disables logs once for function double render', () => {
1061
+ it('does not disable logs for function double render', () => {
1062
spyOnDevAndProd(console, 'log');
1063
1064
let count = 0;
@@ -1077,7 +1077,7 @@ describe('context legacy', () => {
1077
);
1078
1079
expect(count).toBe(__DEV__ ? 2 : 1);
1080
- expect(console.log).toBeCalledTimes(1);
1080
+ expect(console.log).toBeCalledTimes(__DEV__ ? 2 : 1);
1081
// Note: we should display the first log because otherwise
1082
// there is a risk of suppressing warnings when they happen,
1083
// and on the next render they'd get deduplicated and ignored.
packages/scheduler/src/forks/SchedulerMock.js
+9
-2
@@ -79,6 +79,12 @@ let isFlushing: boolean = false;
79
let needsPaint: boolean = false;
80
let shouldYieldForPaint: boolean = false;
81
82
+var disableYieldValue = false;
83
+
84
+function setDisableYieldValue(newValue) {
85
+ disableYieldValue = newValue;
86
+}
87
+
88
function advanceTimers(currentTime) {
89
// Check for tasks that are no longer delayed and add them to the queue.
90
let timer = peek(timerQueue);
@@ -570,7 +576,7 @@ function unstable_flushAll(): void {
576
577
function unstable_yieldValue(value: mixed): void {
578
// eslint-disable-next-line react-internal/no-production-logging
573
- if (console.log.name === 'disabledLog') {
579
+ if (console.log.name === 'disabledLog' || disableYieldValue) {
580
// If console.log has been patched, we assume we're in render
581
// replaying and we ignore any values yielding in the second pass.
582
return;
@@ -584,7 +590,7 @@ function unstable_yieldValue(value: mixed): void {
590
591
function unstable_advanceTime(ms: number) {
592
// eslint-disable-next-line react-internal/no-production-logging
587
- if (console.log.name === 'disabledLog') {
593
+ if (console.log.name === 'disabledLog' || disableYieldValue) {
594
// If console.log has been patched, we assume we're in render
595
// replaying and we ignore any time advancing in the second pass.
596
return;
@@ -629,6 +635,7 @@ export {
635
unstable_yieldValue,
636
unstable_advanceTime,
637
reset,
638
+ setDisableYieldValue as unstable_setDisableYieldValue,
639
};
640
641
export const unstable_Profiling = enableProfiling
packages/shared/consoleWithStackDev.js
+13
-2
@@ -7,6 +7,13 @@
7
8
import ReactSharedInternals from 'shared/ReactSharedInternals';
9
10
+let suppressWarning = false;
11
+export function setSuppressWarning(newSuppressWarning) {
12
+ if (__DEV__) {
13
+ suppressWarning = newSuppressWarning;
14
+ }
15
+}
16
+
17
// In DEV, calls to console.warn and console.error get replaced
18
// by calls to these methods by a Babel plugin.
19
//
@@ -15,13 +22,17 @@ import ReactSharedInternals from 'shared/ReactSharedInternals';
22
23
export function warn(format, ...args) {
24
if (__DEV__) {
18
- printWarning('warn', format, args);
25
+ if (!suppressWarning) {
26
+ printWarning('warn', format, args);
27
+ }
28
}
29
}
30
31
export function error(format, ...args) {
32
if (__DEV__) {
24
- printWarning('error', format, args);
33
+ if (!suppressWarning) {
34
+ printWarning('error', format, args);
35
+ }
36
}
37
}
38
packages/shared/forks/Scheduler.umd.js
+6
-1
@@ -31,8 +31,11 @@ const {
31
unstable_forceFrameRate,
32
33
// this doesn't actually exist on the scheduler, but it *does*
34
- // on scheduler/unstable_mock, which we'll need inside act().
34
+ // on scheduler/unstable_mock, which we'll need inside act()
35
+ // and for internal testing
36
unstable_flushAllWithoutAsserting,
37
+ unstable_yieldValue,
38
+ unstable_setDisableYieldValue,
39
} = ReactInternals.Scheduler;
40
41
export {
@@ -54,4 +57,6 @@ export {
57
unstable_IdlePriority,
58
unstable_forceFrameRate,
59
unstable_flushAllWithoutAsserting,
60
+ unstable_yieldValue,
61
+ unstable_setDisableYieldValue,
62
};
packages/shared/forks/consoleWithStackDev.www.js
+13
-2
@@ -8,15 +8,26 @@
8
// This refers to a WWW module.
9
const warningWWW = require('warning');
10
11
+let suppressWarning = false;
12
+export function setSuppressWarning(newSuppressWarning) {
13
+ if (__DEV__) {
14
+ suppressWarning = newSuppressWarning;
15
+ }
16
+}
17
+
18
export function warn(format, ...args) {
19
if (__DEV__) {
13
- printWarning('warn', format, args);
20
+ if (!suppressWarning) {
21
+ printWarning('warn', format, args);
22
+ }
23
}
24
}
25
26
export function error(format, ...args) {
27
if (__DEV__) {
19
- printWarning('error', format, args);
28
+ if (!suppressWarning) {
29
+ printWarning('error', format, args);
30
+ }
31
}
32
}
33
scripts/flow/react-devtools.js
+7
@@ -9,3 +9,10 @@
9
10
declare var __EXTENSION__: boolean;
11
declare var __TEST__: boolean;
12
+
13
+declare var DARK_MODE_DIMMED_WARNING_COLOR: string;
14
+declare var DARK_MODE_DIMMED_ERROR_COLOR: string;
15
+declare var DARK_MODE_DIMMED_LOG_COLOR: string;
16
+declare var LIGHT_MODE_DIMMED_WARNING_COLOR: string;
17
+declare var LIGHT_MODE_DIMMED_ERROR_COLOR: string;
18
+declare var LIGHT_MODE_DIMMED_LOG_COLOR: string;