mirror of
https://github.com/zebrajr/react.git
synced 2026-01-15 12:15:22 +00:00
Debug render-phase side effects in "strict" mode (#12094)
A new feature flag has been added, debugRenderPhaseSideEffectsForStrictMode. When enabled, StrictMode subtrees will also double-invoke lifecycles in the same way as debugRenderPhaseSideEffects. By default, this flag is enabled for __DEV__ only. Internally we can toggle it with a GK. This breaks several of our incremental tests which make use of the noop-renderer. Updating the tests to account for the double-rendering in development mode makes them significantly more complicated. The most straight forward fix for this will be to convert them to be run as internal tests only. I believe this is reasonable since we are the only people making use of the noop renderer.
This commit is contained in:
@@ -10,12 +10,15 @@
|
||||
'use strict';
|
||||
|
||||
let React;
|
||||
let ReactFeatureFlags;
|
||||
let ReactNoop;
|
||||
let ReactCallReturn;
|
||||
|
||||
describe('ReactCallReturn', () => {
|
||||
beforeEach(() => {
|
||||
jest.resetModules();
|
||||
ReactFeatureFlags = require('shared/ReactFeatureFlags');
|
||||
ReactFeatureFlags.debugRenderPhaseSideEffectsForStrictMode = false;
|
||||
React = require('react');
|
||||
ReactNoop = require('react-noop-renderer');
|
||||
ReactCallReturn = require('react-call-return');
|
||||
@@ -40,7 +40,10 @@ import {
|
||||
Ref,
|
||||
} from 'shared/ReactTypeOfSideEffect';
|
||||
import {ReactCurrentOwner} from 'shared/ReactGlobalSharedState';
|
||||
import {debugRenderPhaseSideEffects} from 'shared/ReactFeatureFlags';
|
||||
import {
|
||||
debugRenderPhaseSideEffects,
|
||||
debugRenderPhaseSideEffectsForStrictMode,
|
||||
} from 'shared/ReactFeatureFlags';
|
||||
import invariant from 'fbjs/lib/invariant';
|
||||
import getComponentName from 'shared/getComponentName';
|
||||
import warning from 'fbjs/lib/warning';
|
||||
@@ -64,7 +67,7 @@ import {
|
||||
} from './ReactFiberContext';
|
||||
import {pushProvider} from './ReactFiberNewContext';
|
||||
import {NoWork, Never} from './ReactFiberExpirationTime';
|
||||
import {AsyncUpdates} from './ReactTypeOfInternalContext';
|
||||
import {AsyncUpdates, StrictMode} from './ReactTypeOfInternalContext';
|
||||
import MAX_SIGNED_31_BIT_INT from './maxSigned31BitInt';
|
||||
|
||||
let didWarnAboutBadClass;
|
||||
@@ -287,12 +290,20 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
|
||||
if (__DEV__) {
|
||||
ReactDebugCurrentFiber.setCurrentPhase('render');
|
||||
nextChildren = instance.render();
|
||||
if (debugRenderPhaseSideEffects) {
|
||||
if (
|
||||
debugRenderPhaseSideEffects ||
|
||||
(debugRenderPhaseSideEffectsForStrictMode &&
|
||||
workInProgress.internalContextTag & StrictMode)
|
||||
) {
|
||||
instance.render();
|
||||
}
|
||||
ReactDebugCurrentFiber.setCurrentPhase(null);
|
||||
} else {
|
||||
if (debugRenderPhaseSideEffects) {
|
||||
if (
|
||||
debugRenderPhaseSideEffects ||
|
||||
(debugRenderPhaseSideEffectsForStrictMode &&
|
||||
workInProgress.internalContextTag & StrictMode)
|
||||
) {
|
||||
instance.render();
|
||||
}
|
||||
nextChildren = instance.render();
|
||||
|
||||
@@ -13,6 +13,7 @@ import type {ExpirationTime} from './ReactFiberExpirationTime';
|
||||
import {Update} from 'shared/ReactTypeOfSideEffect';
|
||||
import {
|
||||
debugRenderPhaseSideEffects,
|
||||
debugRenderPhaseSideEffectsForStrictMode,
|
||||
enableAsyncSubtreeAPI,
|
||||
warnAboutDeprecatedLifecycles,
|
||||
} from 'shared/ReactFeatureFlags';
|
||||
@@ -391,7 +392,11 @@ export default function(
|
||||
: emptyObject;
|
||||
|
||||
// Instantiate twice to help detect side-effects.
|
||||
if (debugRenderPhaseSideEffects) {
|
||||
if (
|
||||
debugRenderPhaseSideEffects ||
|
||||
(debugRenderPhaseSideEffectsForStrictMode &&
|
||||
workInProgress.internalContextTag & StrictMode)
|
||||
) {
|
||||
new ctor(props, context); // eslint-disable-line no-new
|
||||
}
|
||||
|
||||
@@ -537,7 +542,11 @@ export default function(
|
||||
}
|
||||
}
|
||||
|
||||
if (debugRenderPhaseSideEffects) {
|
||||
if (
|
||||
debugRenderPhaseSideEffects ||
|
||||
(debugRenderPhaseSideEffectsForStrictMode &&
|
||||
workInProgress.internalContextTag & StrictMode)
|
||||
) {
|
||||
// Invoke method an extra time to help detect side-effects.
|
||||
type.getDerivedStateFromProps.call(
|
||||
null,
|
||||
|
||||
@@ -10,11 +10,15 @@
|
||||
import type {Fiber} from './ReactFiber';
|
||||
import type {ExpirationTime} from './ReactFiberExpirationTime';
|
||||
|
||||
import {debugRenderPhaseSideEffects} from 'shared/ReactFeatureFlags';
|
||||
import {
|
||||
debugRenderPhaseSideEffects,
|
||||
debugRenderPhaseSideEffectsForStrictMode,
|
||||
} from 'shared/ReactFeatureFlags';
|
||||
import {Callback as CallbackEffect} from 'shared/ReactTypeOfSideEffect';
|
||||
import {ClassComponent, HostRoot} from 'shared/ReactTypeOfWork';
|
||||
import invariant from 'fbjs/lib/invariant';
|
||||
import warning from 'fbjs/lib/warning';
|
||||
import {StrictMode} from './ReactTypeOfInternalContext';
|
||||
|
||||
import {NoWork} from './ReactFiberExpirationTime';
|
||||
|
||||
@@ -183,14 +187,7 @@ export function getUpdateExpirationTime(fiber: Fiber): ExpirationTime {
|
||||
function getStateFromUpdate(update, instance, prevState, props) {
|
||||
const partialState = update.partialState;
|
||||
if (typeof partialState === 'function') {
|
||||
const updateFn = partialState;
|
||||
|
||||
// Invoke setState callback an extra time to help detect side-effects.
|
||||
if (debugRenderPhaseSideEffects) {
|
||||
updateFn.call(instance, prevState, props);
|
||||
}
|
||||
|
||||
return updateFn.call(instance, prevState, props);
|
||||
return partialState.call(instance, prevState, props);
|
||||
} else {
|
||||
return partialState;
|
||||
}
|
||||
@@ -276,6 +273,16 @@ export function processUpdateQueue<State>(
|
||||
}
|
||||
}
|
||||
|
||||
// Invoke setState callback an extra time to help detect side-effects.
|
||||
// Ignore the return value in this case.
|
||||
if (
|
||||
debugRenderPhaseSideEffects ||
|
||||
(debugRenderPhaseSideEffectsForStrictMode &&
|
||||
workInProgress.internalContextTag & StrictMode)
|
||||
) {
|
||||
getStateFromUpdate(update, instance, state, props);
|
||||
}
|
||||
|
||||
// Process the update
|
||||
let partialState;
|
||||
if (update.isReplace) {
|
||||
|
||||
@@ -11,12 +11,15 @@
|
||||
'use strict';
|
||||
|
||||
let React;
|
||||
let ReactFeatureFlags;
|
||||
let ReactNoop;
|
||||
let PropTypes;
|
||||
|
||||
describe('ReactIncremental', () => {
|
||||
beforeEach(() => {
|
||||
jest.resetModules();
|
||||
ReactFeatureFlags = require('shared/ReactFeatureFlags');
|
||||
ReactFeatureFlags.debugRenderPhaseSideEffectsForStrictMode = false;
|
||||
React = require('react');
|
||||
ReactNoop = require('react-noop-renderer');
|
||||
PropTypes = require('prop-types');
|
||||
@@ -11,12 +11,15 @@
|
||||
'use strict';
|
||||
|
||||
let PropTypes;
|
||||
let ReactFeatureFlags;
|
||||
let React;
|
||||
let ReactNoop;
|
||||
|
||||
describe('ReactIncrementalErrorHandling', () => {
|
||||
beforeEach(() => {
|
||||
jest.resetModules();
|
||||
ReactFeatureFlags = require('shared/ReactFeatureFlags');
|
||||
ReactFeatureFlags.debugRenderPhaseSideEffectsForStrictMode = false;
|
||||
PropTypes = require('prop-types');
|
||||
React = require('react');
|
||||
ReactNoop = require('react-noop-renderer');
|
||||
@@ -11,11 +11,14 @@
|
||||
'use strict';
|
||||
|
||||
let React;
|
||||
let ReactFeatureFlags;
|
||||
let ReactNoop;
|
||||
|
||||
describe('ReactIncrementalErrorLogging', () => {
|
||||
beforeEach(() => {
|
||||
jest.resetModules();
|
||||
ReactFeatureFlags = require('shared/ReactFeatureFlags');
|
||||
ReactFeatureFlags.debugRenderPhaseSideEffectsForStrictMode = false;
|
||||
React = require('react');
|
||||
ReactNoop = require('react-noop-renderer');
|
||||
});
|
||||
|
||||
@@ -11,11 +11,14 @@
|
||||
'use strict';
|
||||
|
||||
let React;
|
||||
let ReactFeatureFlags;
|
||||
let ReactNoop;
|
||||
|
||||
describe('ReactIncrementalReflection', () => {
|
||||
beforeEach(() => {
|
||||
jest.resetModules();
|
||||
ReactFeatureFlags = require('shared/ReactFeatureFlags');
|
||||
ReactFeatureFlags.debugRenderPhaseSideEffectsForStrictMode = false;
|
||||
React = require('react');
|
||||
ReactNoop = require('react-noop-renderer');
|
||||
});
|
||||
@@ -11,11 +11,14 @@
|
||||
'use strict';
|
||||
|
||||
let React;
|
||||
let ReactFeatureFlags;
|
||||
let ReactNoop;
|
||||
|
||||
describe('ReactIncrementalScheduling', () => {
|
||||
beforeEach(() => {
|
||||
jest.resetModules();
|
||||
ReactFeatureFlags = require('shared/ReactFeatureFlags');
|
||||
ReactFeatureFlags.debugRenderPhaseSideEffectsForStrictMode = false;
|
||||
React = require('react');
|
||||
ReactNoop = require('react-noop-renderer');
|
||||
});
|
||||
@@ -11,11 +11,14 @@
|
||||
'use strict';
|
||||
|
||||
let React;
|
||||
let ReactFeatureFlags;
|
||||
let ReactNoop;
|
||||
|
||||
describe('ReactIncrementalSideEffects', () => {
|
||||
beforeEach(() => {
|
||||
jest.resetModules();
|
||||
ReactFeatureFlags = require('shared/ReactFeatureFlags');
|
||||
ReactFeatureFlags.debugRenderPhaseSideEffectsForStrictMode = false;
|
||||
React = require('react');
|
||||
ReactNoop = require('react-noop-renderer');
|
||||
});
|
||||
@@ -11,11 +11,14 @@
|
||||
'use strict';
|
||||
|
||||
let React;
|
||||
let ReactFeatureFlags;
|
||||
let ReactNoop;
|
||||
|
||||
describe('ReactIncrementalTriangle', () => {
|
||||
beforeEach(() => {
|
||||
jest.resetModules();
|
||||
ReactFeatureFlags = require('shared/ReactFeatureFlags');
|
||||
ReactFeatureFlags.debugRenderPhaseSideEffectsForStrictMode = false;
|
||||
React = require('react');
|
||||
ReactNoop = require('react-noop-renderer');
|
||||
});
|
||||
@@ -11,11 +11,14 @@
|
||||
'use strict';
|
||||
|
||||
let React;
|
||||
let ReactFeatureFlags;
|
||||
let ReactNoop;
|
||||
|
||||
describe('ReactIncrementalUpdates', () => {
|
||||
beforeEach(() => {
|
||||
jest.resetModuleRegistry();
|
||||
ReactFeatureFlags = require('shared/ReactFeatureFlags');
|
||||
ReactFeatureFlags.debugRenderPhaseSideEffectsForStrictMode = false;
|
||||
React = require('react');
|
||||
ReactNoop = require('react-noop-renderer');
|
||||
});
|
||||
@@ -20,6 +20,7 @@ describe('ReactNewContext', () => {
|
||||
beforeEach(() => {
|
||||
jest.resetModules();
|
||||
ReactFeatureFlags = require('shared/ReactFeatureFlags');
|
||||
ReactFeatureFlags.debugRenderPhaseSideEffectsForStrictMode = false;
|
||||
ReactFeatureFlags.enableNewContextAPI = true;
|
||||
React = require('react');
|
||||
ReactNoop = require('react-noop-renderer');
|
||||
|
||||
@@ -140,6 +140,158 @@ describe('ReactStrictMode', () => {
|
||||
});
|
||||
});
|
||||
|
||||
[true, false].forEach(debugRenderPhaseSideEffectsForStrictMode => {
|
||||
describe(`StrictMode (${debugRenderPhaseSideEffectsForStrictMode})`, () => {
|
||||
beforeEach(() => {
|
||||
jest.resetModules();
|
||||
ReactFeatureFlags = require('shared/ReactFeatureFlags');
|
||||
ReactFeatureFlags.debugRenderPhaseSideEffectsForStrictMode = debugRenderPhaseSideEffectsForStrictMode;
|
||||
React = require('react');
|
||||
ReactTestRenderer = require('react-test-renderer');
|
||||
});
|
||||
|
||||
it('should invoke precommit lifecycle methods twice in DEV', () => {
|
||||
const {StrictMode} = React;
|
||||
|
||||
let log = [];
|
||||
let shouldComponentUpdate = false;
|
||||
|
||||
function Root() {
|
||||
return (
|
||||
<StrictMode>
|
||||
<ClassComponent />
|
||||
</StrictMode>
|
||||
);
|
||||
}
|
||||
|
||||
class ClassComponent extends React.Component {
|
||||
state = {};
|
||||
static getDerivedStateFromProps() {
|
||||
log.push('getDerivedStateFromProps');
|
||||
return null;
|
||||
}
|
||||
constructor(props) {
|
||||
super(props);
|
||||
log.push('constructor');
|
||||
}
|
||||
componentDidMount() {
|
||||
log.push('componentDidMount');
|
||||
}
|
||||
componentDidUpdate() {
|
||||
log.push('componentDidUpdate');
|
||||
}
|
||||
componentWillUnmount() {
|
||||
log.push('componentWillUnmount');
|
||||
}
|
||||
shouldComponentUpdate() {
|
||||
log.push('shouldComponentUpdate');
|
||||
return shouldComponentUpdate;
|
||||
}
|
||||
render() {
|
||||
log.push('render');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
const component = ReactTestRenderer.create(<Root />);
|
||||
|
||||
if (debugRenderPhaseSideEffectsForStrictMode) {
|
||||
expect(log).toEqual([
|
||||
'constructor',
|
||||
'constructor',
|
||||
'getDerivedStateFromProps',
|
||||
'getDerivedStateFromProps',
|
||||
'render',
|
||||
'render',
|
||||
'componentDidMount',
|
||||
]);
|
||||
} else {
|
||||
expect(log).toEqual([
|
||||
'constructor',
|
||||
'getDerivedStateFromProps',
|
||||
'render',
|
||||
'componentDidMount',
|
||||
]);
|
||||
}
|
||||
|
||||
log = [];
|
||||
shouldComponentUpdate = true;
|
||||
|
||||
component.update(<Root />);
|
||||
if (debugRenderPhaseSideEffectsForStrictMode) {
|
||||
expect(log).toEqual([
|
||||
'getDerivedStateFromProps',
|
||||
'getDerivedStateFromProps',
|
||||
'shouldComponentUpdate',
|
||||
'render',
|
||||
'render',
|
||||
'componentDidUpdate',
|
||||
]);
|
||||
} else {
|
||||
expect(log).toEqual([
|
||||
'getDerivedStateFromProps',
|
||||
'shouldComponentUpdate',
|
||||
'render',
|
||||
'componentDidUpdate',
|
||||
]);
|
||||
}
|
||||
|
||||
log = [];
|
||||
shouldComponentUpdate = false;
|
||||
|
||||
component.update(<Root />);
|
||||
if (debugRenderPhaseSideEffectsForStrictMode) {
|
||||
expect(log).toEqual([
|
||||
'getDerivedStateFromProps',
|
||||
'getDerivedStateFromProps',
|
||||
'shouldComponentUpdate',
|
||||
]);
|
||||
} else {
|
||||
expect(log).toEqual([
|
||||
'getDerivedStateFromProps',
|
||||
'shouldComponentUpdate',
|
||||
]);
|
||||
}
|
||||
});
|
||||
|
||||
it('should invoke setState callbacks twice in DEV', () => {
|
||||
const {StrictMode} = React;
|
||||
|
||||
let instance;
|
||||
class ClassComponent extends React.Component {
|
||||
state = {
|
||||
count: 1,
|
||||
};
|
||||
render() {
|
||||
instance = this;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
let setStateCount = 0;
|
||||
|
||||
ReactTestRenderer.create(
|
||||
<StrictMode>
|
||||
<ClassComponent />
|
||||
</StrictMode>,
|
||||
);
|
||||
instance.setState(state => {
|
||||
setStateCount++;
|
||||
return {
|
||||
count: state.count + 1,
|
||||
};
|
||||
});
|
||||
|
||||
// Callback should be invoked twice (in DEV)
|
||||
expect(setStateCount).toBe(
|
||||
debugRenderPhaseSideEffectsForStrictMode ? 2 : 1,
|
||||
);
|
||||
// But each time `state` should be the previous value
|
||||
expect(instance.state.count).toBe(2);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('async subtree', () => {
|
||||
beforeEach(() => {
|
||||
jest.resetModules();
|
||||
|
||||
@@ -26,6 +26,12 @@ export const enableNewContextAPI = false;
|
||||
// Helps identify side effects in begin-phase lifecycle hooks and setState reducers:
|
||||
export const debugRenderPhaseSideEffects = false;
|
||||
|
||||
// In some cases, StrictMode should also double-render lifecycles.
|
||||
// This can be confusing for tests though,
|
||||
// And it can be bad for performance in production.
|
||||
// This feature flag can be used to control the behavior:
|
||||
export const debugRenderPhaseSideEffectsForStrictMode = __DEV__;
|
||||
|
||||
// Warn about deprecated, async-unsafe lifecycles; relates to RFC #6:
|
||||
export const warnAboutDeprecatedLifecycles = false;
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ import typeof * as FeatureFlagsType from 'shared/ReactFeatureFlags';
|
||||
import typeof * as FabricFeatureFlagsType from './ReactFeatureFlags.native-fabric';
|
||||
|
||||
export const debugRenderPhaseSideEffects = false;
|
||||
export const debugRenderPhaseSideEffectsForStrictMode = false;
|
||||
export const enableAsyncSubtreeAPI = true;
|
||||
export const enableCreateRoot = false;
|
||||
export const enableUserTimingAPI = __DEV__;
|
||||
|
||||
@@ -15,6 +15,7 @@ import typeof * as FeatureFlagsShimType from './ReactFeatureFlags.native';
|
||||
// Re-export dynamic flags from the fbsource version.
|
||||
export const {
|
||||
debugRenderPhaseSideEffects,
|
||||
debugRenderPhaseSideEffectsForStrictMode,
|
||||
warnAboutDeprecatedLifecycles,
|
||||
} = require('ReactFeatureFlags');
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ import typeof * as FeatureFlagsShimType from './ReactFeatureFlags.www';
|
||||
// Re-export dynamic flags from the www version.
|
||||
export const {
|
||||
debugRenderPhaseSideEffects,
|
||||
debugRenderPhaseSideEffectsForStrictMode,
|
||||
warnAboutDeprecatedLifecycles,
|
||||
} = require('ReactFeatureFlags');
|
||||
|
||||
|
||||
Reference in New Issue
Block a user