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

This commit is contained in:
Sebastian Silbermann
2024-05-22 11:39:54 +02:00
committed by GitHub
parent 81c5ff2e04
commit 3ac551e855
9 changed files with 272 additions and 30 deletions

View File

@@ -146,6 +146,12 @@ describe('ReactInternalTestUtils', () => {
test('assertLog', async () => {
const Yield = ({id}) => {
Scheduler.log(id);
React.useEffect(() => {
Scheduler.log(`create effect ${id}`);
return () => {
Scheduler.log(`cleanup effect ${id}`);
};
});
return id;
};
@@ -167,7 +173,20 @@ describe('ReactInternalTestUtils', () => {
</React.StrictMode>
);
});
assertLog(['A', 'B', 'C']);
assertLog([
'A',
'B',
'C',
'create effect A',
'create effect B',
'create effect C',
]);
await act(() => {
root.render(null);
});
assertLog(['cleanup effect A', 'cleanup effect B', 'cleanup effect C']);
});
});

View File

@@ -625,6 +625,168 @@ describe('console', () => {
expect(mockGroupCollapsed.mock.calls[0][0]).toBe('groupCollapsed');
});
it('should double log from Effects if hideConsoleLogsInStrictMode is disabled in Strict mode', () => {
global.__REACT_DEVTOOLS_APPEND_COMPONENT_STACK__ = false;
global.__REACT_DEVTOOLS_HIDE_CONSOLE_LOGS_IN_STRICT_MODE__ = false;
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
function App() {
React.useEffect(() => {
fakeConsole.log('log effect create');
fakeConsole.warn('warn effect create');
fakeConsole.error('error effect create');
fakeConsole.info('info effect create');
fakeConsole.group('group effect create');
fakeConsole.groupCollapsed('groupCollapsed effect create');
return () => {
fakeConsole.log('log effect cleanup');
fakeConsole.warn('warn effect cleanup');
fakeConsole.error('error effect cleanup');
fakeConsole.info('info effect cleanup');
fakeConsole.group('group effect cleanup');
fakeConsole.groupCollapsed('groupCollapsed effect cleanup');
};
});
return <div />;
}
act(() =>
root.render(
<React.StrictMode>
<App />
</React.StrictMode>,
),
);
expect(mockLog.mock.calls).toEqual([
['log effect create'],
[
'%c%s',
`color: ${process.env.DARK_MODE_DIMMED_LOG_COLOR}`,
'log effect cleanup',
],
[
'%c%s',
`color: ${process.env.DARK_MODE_DIMMED_LOG_COLOR}`,
'log effect create',
],
]);
expect(mockWarn.mock.calls).toEqual([
['warn effect create'],
[
'%c%s',
`color: ${process.env.DARK_MODE_DIMMED_WARNING_COLOR}`,
'warn effect cleanup',
],
[
'%c%s',
`color: ${process.env.DARK_MODE_DIMMED_WARNING_COLOR}`,
'warn effect create',
],
]);
expect(mockError.mock.calls).toEqual([
['error effect create'],
[
'%c%s',
`color: ${process.env.DARK_MODE_DIMMED_ERROR_COLOR}`,
'error effect cleanup',
],
[
'%c%s',
`color: ${process.env.DARK_MODE_DIMMED_ERROR_COLOR}`,
'error effect create',
],
]);
expect(mockInfo.mock.calls).toEqual([
['info effect create'],
[
'%c%s',
`color: ${process.env.DARK_MODE_DIMMED_LOG_COLOR}`,
'info effect cleanup',
],
[
'%c%s',
`color: ${process.env.DARK_MODE_DIMMED_LOG_COLOR}`,
'info effect create',
],
]);
expect(mockGroup.mock.calls).toEqual([
['group effect create'],
[
'%c%s',
`color: ${process.env.DARK_MODE_DIMMED_LOG_COLOR}`,
'group effect cleanup',
],
[
'%c%s',
`color: ${process.env.DARK_MODE_DIMMED_LOG_COLOR}`,
'group effect create',
],
]);
expect(mockGroupCollapsed.mock.calls).toEqual([
['groupCollapsed effect create'],
[
'%c%s',
`color: ${process.env.DARK_MODE_DIMMED_LOG_COLOR}`,
'groupCollapsed effect cleanup',
],
[
'%c%s',
`color: ${process.env.DARK_MODE_DIMMED_LOG_COLOR}`,
'groupCollapsed effect create',
],
]);
});
it('should not double log from Effects if hideConsoleLogsInStrictMode is enabled in Strict mode', () => {
global.__REACT_DEVTOOLS_APPEND_COMPONENT_STACK__ = false;
global.__REACT_DEVTOOLS_HIDE_CONSOLE_LOGS_IN_STRICT_MODE__ = true;
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
function App() {
React.useEffect(() => {
fakeConsole.log('log effect create');
fakeConsole.warn('warn effect create');
fakeConsole.error('error effect create');
fakeConsole.info('info effect create');
fakeConsole.group('group effect create');
fakeConsole.groupCollapsed('groupCollapsed effect create');
return () => {
fakeConsole.log('log effect cleanup');
fakeConsole.warn('warn effect cleanup');
fakeConsole.error('error effect cleanup');
fakeConsole.info('info effect cleanup');
fakeConsole.group('group effect cleanup');
fakeConsole.groupCollapsed('groupCollapsed effect cleanup');
};
});
return <div />;
}
act(() =>
root.render(
<React.StrictMode>
<App />
</React.StrictMode>,
),
);
expect(mockLog.mock.calls).toEqual([['log effect create']]);
expect(mockWarn.mock.calls).toEqual([['warn effect create']]);
expect(mockError.mock.calls).toEqual([['error effect create']]);
expect(mockInfo.mock.calls).toEqual([['info effect create']]);
expect(mockGroup.mock.calls).toEqual([['group effect create']]);
expect(mockGroupCollapsed.mock.calls).toEqual([
['groupCollapsed effect create'],
]);
});
it('should double log from useMemo if hideConsoleLogsInStrictMode is disabled in Strict mode', () => {
global.__REACT_DEVTOOLS_APPEND_COMPONENT_STACK__ = false;
global.__REACT_DEVTOOLS_HIDE_CONSOLE_LOGS_IN_STRICT_MODE__ = false;

View File

@@ -294,7 +294,7 @@ export function unpatch(): void {
let unpatchForStrictModeFn: null | (() => void) = null;
// NOTE: KEEP IN SYNC with src/hook.js:patchConsoleForInitialRenderInStrictMode
// NOTE: KEEP IN SYNC with src/hook.js:patchConsoleForInitialCommitInStrictMode
export function patchForStrictMode() {
if (consoleManagedByDevToolsDuringStrictMode) {
const overrideConsoleMethods = [
@@ -359,7 +359,7 @@ export function patchForStrictMode() {
}
}
// NOTE: KEEP IN SYNC with src/hook.js:unpatchConsoleForInitialRenderInStrictMode
// NOTE: KEEP IN SYNC with src/hook.js:unpatchConsoleForInitialCommitInStrictMode
export function unpatchForStrictMode(): void {
if (consoleManagedByDevToolsDuringStrictMode) {
if (unpatchForStrictModeFn !== null) {

View File

@@ -75,7 +75,14 @@ export default function DebuggingSettings(_: {}): React.Node {
setHideConsoleLogsInStrictMode(currentTarget.checked)
}
/>{' '}
Hide logs during second render in Strict Mode
Hide logs during additional invocations in{' '}
<a
className={styles.StrictModeLink}
target="_blank"
rel="noopener noreferrer"
href="https://react.dev/reference/react/StrictMode">
Strict Mode
</a>
</label>
</div>
</div>

View File

@@ -141,7 +141,7 @@
border-radius: 0.25rem;
}
.ReleaseNotesLink {
.ReleaseNotesLink, .StrictModeLink {
color: var(--color-button-active);
}
@@ -153,4 +153,4 @@
list-style: none;
padding: 0;
margin: 0;
}
}

View File

@@ -225,7 +225,7 @@ export function installHook(target: any): DevToolsHook | null {
// React and DevTools are connecting and the renderer interface isn't avaiable
// and we want to be able to have consistent logging behavior for double logs
// during the initial renderer.
function patchConsoleForInitialRenderInStrictMode({
function patchConsoleForInitialCommitInStrictMode({
hideConsoleLogsInStrictMode,
browserTheme,
}: {
@@ -311,7 +311,7 @@ export function installHook(target: any): DevToolsHook | null {
}
// NOTE: KEEP IN SYNC with src/backend/console.js:unpatchForStrictMode
function unpatchConsoleForInitialRenderInStrictMode() {
function unpatchConsoleForInitialCommitInStrictMode() {
if (unpatchFn !== null) {
unpatchFn();
unpatchFn = null;
@@ -451,19 +451,19 @@ export function installHook(target: any): DevToolsHook | null {
rendererInterface.unpatchConsoleForStrictMode();
}
} else {
// This should only happen during initial render in the extension before DevTools
// This should only happen during initial commit in the extension before DevTools
// finishes its handshake with the injected renderer
if (isStrictMode) {
const hideConsoleLogsInStrictMode =
window.__REACT_DEVTOOLS_HIDE_CONSOLE_LOGS_IN_STRICT_MODE__ === true;
const browserTheme = window.__REACT_DEVTOOLS_BROWSER_THEME__;
patchConsoleForInitialRenderInStrictMode({
patchConsoleForInitialCommitInStrictMode({
hideConsoleLogsInStrictMode,
browserTheme,
});
} else {
unpatchConsoleForInitialRenderInStrictMode();
unpatchConsoleForInitialCommitInStrictMode();
}
}
}

View File

@@ -251,6 +251,7 @@ import {
markRenderStopped,
onCommitRoot as onCommitRootDevTools,
onPostCommitRoot as onPostCommitRootDevTools,
setIsStrictModeForDevtools,
} from './ReactFiberDevToolsHook';
import {onCommitRoot as onCommitRootTestSelector} from './ReactTestSelectors';
import {releaseCache} from './ReactFiberCacheComponent';
@@ -3675,6 +3676,7 @@ function doubleInvokeEffectsOnFiber(
fiber: Fiber,
shouldDoubleInvokePassiveEffects: boolean = true,
) {
setIsStrictModeForDevtools(true);
disappearLayoutEffects(fiber);
if (shouldDoubleInvokePassiveEffects) {
disconnectPassiveEffect(fiber);
@@ -3683,6 +3685,7 @@ function doubleInvokeEffectsOnFiber(
if (shouldDoubleInvokePassiveEffects) {
reconnectPassiveEffects(root, fiber, NoLanes, null, false);
}
setIsStrictModeForDevtools(false);
}
function doubleInvokeEffectsInDEVIfNecessary(

View File

@@ -115,11 +115,7 @@ describe('StrictEffectsMode defaults', () => {
</React.StrictMode>,
);
await waitForPaint([
'useLayoutEffect mount "one"',
'useLayoutEffect unmount "one"',
'useLayoutEffect mount "one"',
]);
await waitForPaint(['useLayoutEffect mount "one"']);
expect(log).toEqual([
'useLayoutEffect mount "one"',
'useLayoutEffect unmount "one"',
@@ -142,10 +138,6 @@ describe('StrictEffectsMode defaults', () => {
'useLayoutEffect unmount "one"',
'useLayoutEffect mount "one"',
'useLayoutEffect mount "two"',
// Since "two" is new, it should be double-invoked.
'useLayoutEffect unmount "two"',
'useLayoutEffect mount "two"',
]);
expect(log).toEqual([
// Cleanup and re-run "one" (and "two") since there is no dependencies array.
@@ -196,10 +188,6 @@ describe('StrictEffectsMode defaults', () => {
await waitForAll([
'useLayoutEffect mount "one"',
'useEffect mount "one"',
'useLayoutEffect unmount "one"',
'useEffect unmount "one"',
'useLayoutEffect mount "one"',
'useEffect mount "one"',
]);
expect(log).toEqual([
'useLayoutEffect mount "one"',
@@ -237,12 +225,6 @@ describe('StrictEffectsMode defaults', () => {
'useEffect unmount "one"',
'useEffect mount "one"',
'useEffect mount "two"',
// Since "two" is new, it should be double-invoked.
'useLayoutEffect unmount "two"',
'useEffect unmount "two"',
'useLayoutEffect mount "two"',
'useEffect mount "two"',
]);
expect(log).toEqual([
'useEffect unmount "one"',

View File

@@ -1343,6 +1343,42 @@ describe('context legacy', () => {
// and on the next render they'd get deduplicated and ignored.
expect(console.log).toBeCalledWith('foo 1');
});
it('does not disable logs for effect double invoke', async () => {
let create = 0;
let cleanup = 0;
function Foo() {
React.useEffect(() => {
create++;
console.log('foo create ' + create);
return () => {
cleanup++;
console.log('foo cleanup ' + cleanup);
};
});
return null;
}
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
await act(() => {
root.render(
<React.StrictMode>
<Foo />
</React.StrictMode>,
);
});
expect(create).toBe(__DEV__ ? 2 : 1);
expect(cleanup).toBe(__DEV__ ? 1 : 0);
expect(console.log).toBeCalledTimes(__DEV__ ? 3 : 1);
// Note: we should display the first log because otherwise
// there is a risk of suppressing warnings when they happen,
// and on the next render they'd get deduplicated and ignored.
expect(console.log).toBeCalledWith('foo create 1');
if (__DEV__) {
expect(console.log).toBeCalledWith('foo cleanup 1');
}
});
} else {
it('disable logs for class double render', async () => {
let count = 0;
@@ -1530,6 +1566,39 @@ describe('context legacy', () => {
// and on the next render they'd get deduplicated and ignored.
expect(console.log).toBeCalledWith('foo 1');
});
it('disable logs for effect double invoke', async () => {
let create = 0;
let cleanup = 0;
function Foo() {
React.useEffect(() => {
create++;
console.log('foo create ' + create);
return () => {
cleanup++;
console.log('foo cleanup ' + cleanup);
};
});
return null;
}
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
await act(() => {
root.render(
<React.StrictMode>
<Foo />
</React.StrictMode>,
);
});
expect(create).toBe(__DEV__ ? 2 : 1);
expect(cleanup).toBe(__DEV__ ? 1 : 0);
expect(console.log).toBeCalledTimes(1);
// Note: we should display the first log because otherwise
// there is a risk of suppressing warnings when they happen,
// and on the next render they'd get deduplicated and ignored.
expect(console.log).toBeCalledWith('foo create 1');
});
}
});
});