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:
Brian Vaughn
2018-01-25 14:30:53 -08:00
committed by GitHub
parent 6dabfca577
commit d3b183c323
18 changed files with 231 additions and 15 deletions

View File

@@ -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');

View File

@@ -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();

View File

@@ -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,

View File

@@ -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) {

View File

@@ -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');

View File

@@ -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');

View File

@@ -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');
});

View File

@@ -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');
});

View File

@@ -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');
});

View File

@@ -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');
});

View File

@@ -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');
});

View File

@@ -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');
});

View File

@@ -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');

View File

@@ -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();

View File

@@ -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;

View File

@@ -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__;

View File

@@ -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');

View File

@@ -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');