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.
Brian Vaughn committed
Jan 25, 2018 at 14:30 UTC
d3b183c32326cacc29efea43ca9300a17ed4aca0
18 files changed
+231
-15
packages/react-call-return/src/__tests__/ReactCallReturn-test.internal.js
renamed
+3
@@ -10,12 +10,15 @@
10
'use strict';
11
12
let React;
13
+let ReactFeatureFlags;
14
let ReactNoop;
15
let ReactCallReturn;
16
17
describe('ReactCallReturn', () => {
18
beforeEach(() => {
19
jest.resetModules();
20
+ ReactFeatureFlags = require('shared/ReactFeatureFlags');
21
+ ReactFeatureFlags.debugRenderPhaseSideEffectsForStrictMode = false;
22
React = require('react');
23
ReactNoop = require('react-noop-renderer');
24
ReactCallReturn = require('react-call-return');
packages/react-reconciler/src/ReactFiberBeginWork.js
+15
-4
@@ -40,7 +40,10 @@ import {
40
Ref,
41
} from 'shared/ReactTypeOfSideEffect';
42
import {ReactCurrentOwner} from 'shared/ReactGlobalSharedState';
43
-import {debugRenderPhaseSideEffects} from 'shared/ReactFeatureFlags';
43
+import {
44
+ debugRenderPhaseSideEffects,
45
+ debugRenderPhaseSideEffectsForStrictMode,
46
+} from 'shared/ReactFeatureFlags';
47
import invariant from 'fbjs/lib/invariant';
48
import getComponentName from 'shared/getComponentName';
49
import warning from 'fbjs/lib/warning';
@@ -64,7 +67,7 @@ import {
67
} from './ReactFiberContext';
68
import {pushProvider} from './ReactFiberNewContext';
69
import {NoWork, Never} from './ReactFiberExpirationTime';
67
-import {AsyncUpdates} from './ReactTypeOfInternalContext';
70
+import {AsyncUpdates, StrictMode} from './ReactTypeOfInternalContext';
71
import MAX_SIGNED_31_BIT_INT from './maxSigned31BitInt';
72
73
let didWarnAboutBadClass;
@@ -287,12 +290,20 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
290
if (__DEV__) {
291
ReactDebugCurrentFiber.setCurrentPhase('render');
292
nextChildren = instance.render();
290
- if (debugRenderPhaseSideEffects) {
293
+ if (
294
+ debugRenderPhaseSideEffects ||
295
+ (debugRenderPhaseSideEffectsForStrictMode &&
296
+ workInProgress.internalContextTag & StrictMode)
297
+ ) {
298
instance.render();
299
}
300
ReactDebugCurrentFiber.setCurrentPhase(null);
301
} else {
295
- if (debugRenderPhaseSideEffects) {
302
+ if (
303
+ debugRenderPhaseSideEffects ||
304
+ (debugRenderPhaseSideEffectsForStrictMode &&
305
+ workInProgress.internalContextTag & StrictMode)
306
+ ) {
307
instance.render();
308
}
309
nextChildren = instance.render();
packages/react-reconciler/src/ReactFiberClassComponent.js
+11
-2
@@ -13,6 +13,7 @@ import type {ExpirationTime} from './ReactFiberExpirationTime';
13
import {Update} from 'shared/ReactTypeOfSideEffect';
14
import {
15
debugRenderPhaseSideEffects,
16
+ debugRenderPhaseSideEffectsForStrictMode,
17
enableAsyncSubtreeAPI,
18
warnAboutDeprecatedLifecycles,
19
} from 'shared/ReactFeatureFlags';
@@ -391,7 +392,11 @@ export default function(
392
: emptyObject;
393
394
// Instantiate twice to help detect side-effects.
394
- if (debugRenderPhaseSideEffects) {
395
+ if (
396
+ debugRenderPhaseSideEffects ||
397
+ (debugRenderPhaseSideEffectsForStrictMode &&
398
+ workInProgress.internalContextTag & StrictMode)
399
+ ) {
400
new ctor(props, context); // eslint-disable-line no-new
401
}
402
@@ -537,7 +542,11 @@ export default function(
542
}
543
}
544
540
- if (debugRenderPhaseSideEffects) {
545
+ if (
546
+ debugRenderPhaseSideEffects ||
547
+ (debugRenderPhaseSideEffectsForStrictMode &&
548
+ workInProgress.internalContextTag & StrictMode)
549
+ ) {
550
// Invoke method an extra time to help detect side-effects.
551
type.getDerivedStateFromProps.call(
552
null,
packages/react-reconciler/src/ReactFiberUpdateQueue.js
+16
-9
@@ -10,11 +10,15 @@
10
import type {Fiber} from './ReactFiber';
11
import type {ExpirationTime} from './ReactFiberExpirationTime';
12
13
-import {debugRenderPhaseSideEffects} from 'shared/ReactFeatureFlags';
13
+import {
14
+ debugRenderPhaseSideEffects,
15
+ debugRenderPhaseSideEffectsForStrictMode,
16
+} from 'shared/ReactFeatureFlags';
17
import {Callback as CallbackEffect} from 'shared/ReactTypeOfSideEffect';
18
import {ClassComponent, HostRoot} from 'shared/ReactTypeOfWork';
19
import invariant from 'fbjs/lib/invariant';
20
import warning from 'fbjs/lib/warning';
21
+import {StrictMode} from './ReactTypeOfInternalContext';
22
23
import {NoWork} from './ReactFiberExpirationTime';
24
@@ -183,14 +187,7 @@ export function getUpdateExpirationTime(fiber: Fiber): ExpirationTime {
187
function getStateFromUpdate(update, instance, prevState, props) {
188
const partialState = update.partialState;
189
if (typeof partialState === 'function') {
186
- const updateFn = partialState;
187
-
188
- // Invoke setState callback an extra time to help detect side-effects.
189
- if (debugRenderPhaseSideEffects) {
190
- updateFn.call(instance, prevState, props);
191
- }
192
-
193
- return updateFn.call(instance, prevState, props);
190
+ return partialState.call(instance, prevState, props);
191
} else {
192
return partialState;
193
}
@@ -276,6 +273,16 @@ export function processUpdateQueue<State>(
273
}
274
}
275
276
+ // Invoke setState callback an extra time to help detect side-effects.
277
+ // Ignore the return value in this case.
278
+ if (
279
+ debugRenderPhaseSideEffects ||
280
+ (debugRenderPhaseSideEffectsForStrictMode &&
281
+ workInProgress.internalContextTag & StrictMode)
282
+ ) {
283
+ getStateFromUpdate(update, instance, state, props);
284
+ }
285
+
286
// Process the update
287
let partialState;
288
if (update.isReplace) {
packages/react-reconciler/src/__tests__/ReactIncremental-test.internal.js
renamed
+3
@@ -11,12 +11,15 @@
11
'use strict';
12
13
let React;
14
+let ReactFeatureFlags;
15
let ReactNoop;
16
let PropTypes;
17
18
describe('ReactIncremental', () => {
19
beforeEach(() => {
20
jest.resetModules();
21
+ ReactFeatureFlags = require('shared/ReactFeatureFlags');
22
+ ReactFeatureFlags.debugRenderPhaseSideEffectsForStrictMode = false;
23
React = require('react');
24
ReactNoop = require('react-noop-renderer');
25
PropTypes = require('prop-types');
packages/react-reconciler/src/__tests__/ReactIncrementalErrorHandling-test.internal.js
renamed
+3
@@ -11,12 +11,15 @@
11
'use strict';
12
13
let PropTypes;
14
+let ReactFeatureFlags;
15
let React;
16
let ReactNoop;
17
18
describe('ReactIncrementalErrorHandling', () => {
19
beforeEach(() => {
20
jest.resetModules();
21
+ ReactFeatureFlags = require('shared/ReactFeatureFlags');
22
+ ReactFeatureFlags.debugRenderPhaseSideEffectsForStrictMode = false;
23
PropTypes = require('prop-types');
24
React = require('react');
25
ReactNoop = require('react-noop-renderer');
packages/react-reconciler/src/__tests__/ReactIncrementalErrorLogging-test.internal.js
+3
@@ -11,11 +11,14 @@
11
'use strict';
12
13
let React;
14
+let ReactFeatureFlags;
15
let ReactNoop;
16
17
describe('ReactIncrementalErrorLogging', () => {
18
beforeEach(() => {
19
jest.resetModules();
20
+ ReactFeatureFlags = require('shared/ReactFeatureFlags');
21
+ ReactFeatureFlags.debugRenderPhaseSideEffectsForStrictMode = false;
22
React = require('react');
23
ReactNoop = require('react-noop-renderer');
24
});
packages/react-reconciler/src/__tests__/ReactIncrementalReflection-test.internal.js
renamed
+3
@@ -11,11 +11,14 @@
11
'use strict';
12
13
let React;
14
+let ReactFeatureFlags;
15
let ReactNoop;
16
17
describe('ReactIncrementalReflection', () => {
18
beforeEach(() => {
19
jest.resetModules();
20
+ ReactFeatureFlags = require('shared/ReactFeatureFlags');
21
+ ReactFeatureFlags.debugRenderPhaseSideEffectsForStrictMode = false;
22
React = require('react');
23
ReactNoop = require('react-noop-renderer');
24
});
packages/react-reconciler/src/__tests__/ReactIncrementalScheduling-test.internal.js
renamed
+3
@@ -11,11 +11,14 @@
11
'use strict';
12
13
let React;
14
+let ReactFeatureFlags;
15
let ReactNoop;
16
17
describe('ReactIncrementalScheduling', () => {
18
beforeEach(() => {
19
jest.resetModules();
20
+ ReactFeatureFlags = require('shared/ReactFeatureFlags');
21
+ ReactFeatureFlags.debugRenderPhaseSideEffectsForStrictMode = false;
22
React = require('react');
23
ReactNoop = require('react-noop-renderer');
24
});
packages/react-reconciler/src/__tests__/ReactIncrementalSideEffects-test.internal.js
renamed
+3
@@ -11,11 +11,14 @@
11
'use strict';
12
13
let React;
14
+let ReactFeatureFlags;
15
let ReactNoop;
16
17
describe('ReactIncrementalSideEffects', () => {
18
beforeEach(() => {
19
jest.resetModules();
20
+ ReactFeatureFlags = require('shared/ReactFeatureFlags');
21
+ ReactFeatureFlags.debugRenderPhaseSideEffectsForStrictMode = false;
22
React = require('react');
23
ReactNoop = require('react-noop-renderer');
24
});
packages/react-reconciler/src/__tests__/ReactIncrementalTriangle-test.internal.js
renamed
+3
@@ -11,11 +11,14 @@
11
'use strict';
12
13
let React;
14
+let ReactFeatureFlags;
15
let ReactNoop;
16
17
describe('ReactIncrementalTriangle', () => {
18
beforeEach(() => {
19
jest.resetModules();
20
+ ReactFeatureFlags = require('shared/ReactFeatureFlags');
21
+ ReactFeatureFlags.debugRenderPhaseSideEffectsForStrictMode = false;
22
React = require('react');
23
ReactNoop = require('react-noop-renderer');
24
});
packages/react-reconciler/src/__tests__/ReactIncrementalUpdates-test.internal.js
renamed
+3
@@ -11,11 +11,14 @@
11
'use strict';
12
13
let React;
14
+let ReactFeatureFlags;
15
let ReactNoop;
16
17
describe('ReactIncrementalUpdates', () => {
18
beforeEach(() => {
19
jest.resetModuleRegistry();
20
+ ReactFeatureFlags = require('shared/ReactFeatureFlags');
21
+ ReactFeatureFlags.debugRenderPhaseSideEffectsForStrictMode = false;
22
React = require('react');
23
ReactNoop = require('react-noop-renderer');
24
});
packages/react-reconciler/src/__tests__/ReactNewContext-test.internal.js
+1
@@ -20,6 +20,7 @@ describe('ReactNewContext', () => {
20
beforeEach(() => {
21
jest.resetModules();
22
ReactFeatureFlags = require('shared/ReactFeatureFlags');
23
+ ReactFeatureFlags.debugRenderPhaseSideEffectsForStrictMode = false;
24
ReactFeatureFlags.enableNewContextAPI = true;
25
React = require('react');
26
ReactNoop = require('react-noop-renderer');
packages/react/src/__tests__/ReactStrictMode-test.internal.js
+152
@@ -140,6 +140,158 @@ describe('ReactStrictMode', () => {
140
});
141
});
142
143
+ [true, false].forEach(debugRenderPhaseSideEffectsForStrictMode => {
144
+ describe(`StrictMode (${debugRenderPhaseSideEffectsForStrictMode})`, () => {
145
+ beforeEach(() => {
146
+ jest.resetModules();
147
+ ReactFeatureFlags = require('shared/ReactFeatureFlags');
148
+ ReactFeatureFlags.debugRenderPhaseSideEffectsForStrictMode = debugRenderPhaseSideEffectsForStrictMode;
149
+ React = require('react');
150
+ ReactTestRenderer = require('react-test-renderer');
151
+ });
152
+
153
+ it('should invoke precommit lifecycle methods twice in DEV', () => {
154
+ const {StrictMode} = React;
155
+
156
+ let log = [];
157
+ let shouldComponentUpdate = false;
158
+
159
+ function Root() {
160
+ return (
161
+ <StrictMode>
162
+ <ClassComponent />
163
+ </StrictMode>
164
+ );
165
+ }
166
+
167
+ class ClassComponent extends React.Component {
168
+ state = {};
169
+ static getDerivedStateFromProps() {
170
+ log.push('getDerivedStateFromProps');
171
+ return null;
172
+ }
173
+ constructor(props) {
174
+ super(props);
175
+ log.push('constructor');
176
+ }
177
+ componentDidMount() {
178
+ log.push('componentDidMount');
179
+ }
180
+ componentDidUpdate() {
181
+ log.push('componentDidUpdate');
182
+ }
183
+ componentWillUnmount() {
184
+ log.push('componentWillUnmount');
185
+ }
186
+ shouldComponentUpdate() {
187
+ log.push('shouldComponentUpdate');
188
+ return shouldComponentUpdate;
189
+ }
190
+ render() {
191
+ log.push('render');
192
+ return null;
193
+ }
194
+ }
195
+
196
+ const component = ReactTestRenderer.create(<Root />);
197
+
198
+ if (debugRenderPhaseSideEffectsForStrictMode) {
199
+ expect(log).toEqual([
200
+ 'constructor',
201
+ 'constructor',
202
+ 'getDerivedStateFromProps',
203
+ 'getDerivedStateFromProps',
204
+ 'render',
205
+ 'render',
206
+ 'componentDidMount',
207
+ ]);
208
+ } else {
209
+ expect(log).toEqual([
210
+ 'constructor',
211
+ 'getDerivedStateFromProps',
212
+ 'render',
213
+ 'componentDidMount',
214
+ ]);
215
+ }
216
+
217
+ log = [];
218
+ shouldComponentUpdate = true;
219
+
220
+ component.update(<Root />);
221
+ if (debugRenderPhaseSideEffectsForStrictMode) {
222
+ expect(log).toEqual([
223
+ 'getDerivedStateFromProps',
224
+ 'getDerivedStateFromProps',
225
+ 'shouldComponentUpdate',
226
+ 'render',
227
+ 'render',
228
+ 'componentDidUpdate',
229
+ ]);
230
+ } else {
231
+ expect(log).toEqual([
232
+ 'getDerivedStateFromProps',
233
+ 'shouldComponentUpdate',
234
+ 'render',
235
+ 'componentDidUpdate',
236
+ ]);
237
+ }
238
+
239
+ log = [];
240
+ shouldComponentUpdate = false;
241
+
242
+ component.update(<Root />);
243
+ if (debugRenderPhaseSideEffectsForStrictMode) {
244
+ expect(log).toEqual([
245
+ 'getDerivedStateFromProps',
246
+ 'getDerivedStateFromProps',
247
+ 'shouldComponentUpdate',
248
+ ]);
249
+ } else {
250
+ expect(log).toEqual([
251
+ 'getDerivedStateFromProps',
252
+ 'shouldComponentUpdate',
253
+ ]);
254
+ }
255
+ });
256
+
257
+ it('should invoke setState callbacks twice in DEV', () => {
258
+ const {StrictMode} = React;
259
+
260
+ let instance;
261
+ class ClassComponent extends React.Component {
262
+ state = {
263
+ count: 1,
264
+ };
265
+ render() {
266
+ instance = this;
267
+ return null;
268
+ }
269
+ }
270
+
271
+ let setStateCount = 0;
272
+
273
+ ReactTestRenderer.create(
274
+ <StrictMode>
275
+ <ClassComponent />
276
+ </StrictMode>,
277
+ );
278
+ instance.setState(state => {
279
+ setStateCount++;
280
+ return {
281
+ count: state.count + 1,
282
+ };
283
+ });
284
+
285
+ // Callback should be invoked twice (in DEV)
286
+ expect(setStateCount).toBe(
287
+ debugRenderPhaseSideEffectsForStrictMode ? 2 : 1,
288
+ );
289
+ // But each time `state` should be the previous value
290
+ expect(instance.state.count).toBe(2);
291
+ });
292
+ });
293
+ });
294
+
295
describe('async subtree', () => {
296
beforeEach(() => {
297
jest.resetModules();
packages/shared/ReactFeatureFlags.js
+6
@@ -26,6 +26,12 @@ export const enableNewContextAPI = false;
26
// Helps identify side effects in begin-phase lifecycle hooks and setState reducers:
27
export const debugRenderPhaseSideEffects = false;
28
29
+// In some cases, StrictMode should also double-render lifecycles.
30
+// This can be confusing for tests though,
31
+// And it can be bad for performance in production.
32
+// This feature flag can be used to control the behavior:
33
+export const debugRenderPhaseSideEffectsForStrictMode = __DEV__;
34
+
35
// Warn about deprecated, async-unsafe lifecycles; relates to RFC #6:
36
export const warnAboutDeprecatedLifecycles = false;
37
packages/shared/forks/ReactFeatureFlags.native-fabric.js
+1
@@ -13,6 +13,7 @@ import typeof * as FeatureFlagsType from 'shared/ReactFeatureFlags';
13
import typeof * as FabricFeatureFlagsType from './ReactFeatureFlags.native-fabric';
14
15
export const debugRenderPhaseSideEffects = false;
16
+export const debugRenderPhaseSideEffectsForStrictMode = false;
17
export const enableAsyncSubtreeAPI = true;
18
export const enableCreateRoot = false;
19
export const enableUserTimingAPI = __DEV__;
packages/shared/forks/ReactFeatureFlags.native.js
+1
@@ -15,6 +15,7 @@ import typeof * as FeatureFlagsShimType from './ReactFeatureFlags.native';
15
// Re-export dynamic flags from the fbsource version.
16
export const {
17
debugRenderPhaseSideEffects,
18
+ debugRenderPhaseSideEffectsForStrictMode,
19
warnAboutDeprecatedLifecycles,
20
} = require('ReactFeatureFlags');
21
packages/shared/forks/ReactFeatureFlags.www.js
+1
@@ -13,6 +13,7 @@ import typeof * as FeatureFlagsShimType from './ReactFeatureFlags.www';
13
// Re-export dynamic flags from the www version.
14
export const {
15
debugRenderPhaseSideEffects,
16
+ debugRenderPhaseSideEffectsForStrictMode,
17
warnAboutDeprecatedLifecycles,
18
} = require('ReactFeatureFlags');
19