Skip to content

Commit 7f68544

Browse files
authored
New feature flags to help detect unexpected lifecycle side effects (#11587)
Added `debugRenderPhaseSideEffects` feature flag to help detect unexpected side effects in pre-commit lifecycle hooks and `setState` reducers.
1 parent b5e4b45 commit 7f68544

File tree

8 files changed

+167
-2
lines changed

8 files changed

+167
-2
lines changed

packages/react-cs-renderer/src/ReactNativeCSFeatureFlags.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import invariant from 'fbjs/lib/invariant';
1212
import typeof * as FeatureFlagsType from 'shared/ReactFeatureFlags';
1313
import typeof * as CSFeatureFlagsType from './ReactNativeCSFeatureFlags';
1414

15+
export const debugRenderPhaseSideEffects = false;
1516
export const enableAsyncSubtreeAPI = true;
1617
export const enableAsyncSchedulingByDefaultInReactDOM = false;
1718
export const enableReactFragment = false;

packages/react-reconciler/src/ReactFiberBeginWork.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ import {
3636
Ref,
3737
} from 'shared/ReactTypeOfSideEffect';
3838
import {ReactCurrentOwner} from 'shared/ReactGlobalSharedState';
39+
import {debugRenderPhaseSideEffects} from 'shared/ReactFeatureFlags';
3940
import invariant from 'fbjs/lib/invariant';
4041
import getComponentName from 'shared/getComponentName';
4142
import warning from 'fbjs/lib/warning';
@@ -269,8 +270,14 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
269270
if (__DEV__) {
270271
ReactDebugCurrentFiber.setCurrentPhase('render');
271272
nextChildren = instance.render();
273+
if (debugRenderPhaseSideEffects) {
274+
instance.render();
275+
}
272276
ReactDebugCurrentFiber.setCurrentPhase(null);
273277
} else {
278+
if (debugRenderPhaseSideEffects) {
279+
instance.render();
280+
}
274281
nextChildren = instance.render();
275282
}
276283
// React DevTools reads this flag.

packages/react-reconciler/src/ReactFiberClassComponent.js

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,10 @@ import type {Fiber} from './ReactFiber';
1111
import type {ExpirationTime} from './ReactFiberExpirationTime';
1212

1313
import {Update} from 'shared/ReactTypeOfSideEffect';
14-
import {enableAsyncSubtreeAPI} from 'shared/ReactFeatureFlags';
14+
import {
15+
debugRenderPhaseSideEffects,
16+
enableAsyncSubtreeAPI,
17+
} from 'shared/ReactFeatureFlags';
1518
import {isMounted} from 'shared/ReactFiberTreeReflection';
1619
import * as ReactInstanceMap from 'shared/ReactInstanceMap';
1720
import emptyObject from 'fbjs/lib/emptyObject';
@@ -168,6 +171,11 @@ export default function(
168171
);
169172
stopPhaseTimer();
170173

174+
// Simulate an async bailout/interruption by invoking lifecycle twice.
175+
if (debugRenderPhaseSideEffects) {
176+
instance.shouldComponentUpdate(newProps, newState, newContext);
177+
}
178+
171179
if (__DEV__) {
172180
warning(
173181
shouldUpdate !== undefined,
@@ -374,9 +382,13 @@ export default function(
374382
startPhaseTimer(workInProgress, 'componentWillMount');
375383
const oldState = instance.state;
376384
instance.componentWillMount();
377-
378385
stopPhaseTimer();
379386

387+
// Simulate an async bailout/interruption by invoking lifecycle twice.
388+
if (debugRenderPhaseSideEffects) {
389+
instance.componentWillMount();
390+
}
391+
380392
if (oldState !== instance.state) {
381393
if (__DEV__) {
382394
warning(
@@ -402,6 +414,11 @@ export default function(
402414
instance.componentWillReceiveProps(newProps, newContext);
403415
stopPhaseTimer();
404416

417+
// Simulate an async bailout/interruption by invoking lifecycle twice.
418+
if (debugRenderPhaseSideEffects) {
419+
instance.componentWillReceiveProps(newProps, newContext);
420+
}
421+
405422
if (instance.state !== oldState) {
406423
if (__DEV__) {
407424
const componentName = getComponentName(workInProgress) || 'Component';
@@ -677,6 +694,11 @@ export default function(
677694
startPhaseTimer(workInProgress, 'componentWillUpdate');
678695
instance.componentWillUpdate(newProps, newState, newContext);
679696
stopPhaseTimer();
697+
698+
// Simulate an async bailout/interruption by invoking lifecycle twice.
699+
if (debugRenderPhaseSideEffects) {
700+
instance.componentWillUpdate(newProps, newState, newContext);
701+
}
680702
}
681703
if (typeof instance.componentDidUpdate === 'function') {
682704
workInProgress.effectTag |= Update;

packages/react-reconciler/src/ReactFiberScheduler.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -620,6 +620,7 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
620620
if (__DEV__) {
621621
ReactDebugCurrentFiber.setCurrentFiber(workInProgress);
622622
}
623+
623624
let next = beginWork(current, workInProgress, nextRenderExpirationTime);
624625
if (__DEV__) {
625626
ReactDebugCurrentFiber.resetCurrentFiber();

packages/react-reconciler/src/ReactFiberUpdateQueue.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import type {Fiber} from './ReactFiber';
1111
import type {ExpirationTime} from './ReactFiberExpirationTime';
1212

13+
import {debugRenderPhaseSideEffects} from 'shared/ReactFeatureFlags';
1314
import {Callback as CallbackEffect} from 'shared/ReactTypeOfSideEffect';
1415
import {ClassComponent, HostRoot} from 'shared/ReactTypeOfWork';
1516
import invariant from 'fbjs/lib/invariant';
@@ -181,6 +182,12 @@ function getStateFromUpdate(update, instance, prevState, props) {
181182
const partialState = update.partialState;
182183
if (typeof partialState === 'function') {
183184
const updateFn = partialState;
185+
186+
// Invoke setState callback an extra time to help detect side-effects.
187+
if (debugRenderPhaseSideEffects) {
188+
updateFn.call(instance, prevState, props);
189+
}
190+
184191
return updateFn.call(instance, prevState, props);
185192
} else {
186193
return partialState;
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
/**
2+
* Copyright (c) 2013-present, Facebook, Inc.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
* @emails react-core
8+
*/
9+
10+
'use strict';
11+
12+
var React;
13+
var ReactFeatureFlags;
14+
var ReactTestRenderer;
15+
16+
describe('ReactAsyncClassComponent', () => {
17+
describe('debugRenderPhaseSideEffects', () => {
18+
beforeEach(() => {
19+
jest.resetModules();
20+
ReactFeatureFlags = require('shared/ReactFeatureFlags');
21+
ReactFeatureFlags.debugRenderPhaseSideEffects = true;
22+
React = require('react');
23+
ReactTestRenderer = require('react-test-renderer');
24+
});
25+
26+
it('should invoke precommit lifecycle methods twice', () => {
27+
let log = [];
28+
let shouldComponentUpdate = false;
29+
class ClassComponent extends React.Component {
30+
state = {};
31+
componentDidMount() {
32+
log.push('componentDidMount');
33+
}
34+
componentDidUpdate() {
35+
log.push('componentDidUpdate');
36+
}
37+
componentWillMount() {
38+
log.push('componentWillMount');
39+
}
40+
componentWillReceiveProps() {
41+
log.push('componentWillReceiveProps');
42+
}
43+
componentWillUnmount() {
44+
log.push('componentWillUnmount');
45+
}
46+
componentWillUpdate() {
47+
log.push('componentWillUpdate');
48+
}
49+
shouldComponentUpdate() {
50+
log.push('shouldComponentUpdate');
51+
return shouldComponentUpdate;
52+
}
53+
render() {
54+
log.push('render');
55+
return null;
56+
}
57+
}
58+
59+
const component = ReactTestRenderer.create(<ClassComponent />);
60+
expect(log).toEqual([
61+
'componentWillMount',
62+
'componentWillMount',
63+
'render',
64+
'render',
65+
'componentDidMount',
66+
]);
67+
68+
log = [];
69+
shouldComponentUpdate = true;
70+
71+
component.update(<ClassComponent />);
72+
expect(log).toEqual([
73+
'componentWillReceiveProps',
74+
'componentWillReceiveProps',
75+
'shouldComponentUpdate',
76+
'shouldComponentUpdate',
77+
'componentWillUpdate',
78+
'componentWillUpdate',
79+
'render',
80+
'render',
81+
'componentDidUpdate',
82+
]);
83+
84+
log = [];
85+
shouldComponentUpdate = false;
86+
87+
component.update(<ClassComponent />);
88+
expect(log).toEqual([
89+
'componentWillReceiveProps',
90+
'componentWillReceiveProps',
91+
'shouldComponentUpdate',
92+
'shouldComponentUpdate',
93+
]);
94+
});
95+
96+
it('should invoke setState callbacks twice', () => {
97+
class ClassComponent extends React.Component {
98+
state = {
99+
count: 1,
100+
};
101+
render() {
102+
return null;
103+
}
104+
}
105+
106+
let setStateCount = 0;
107+
108+
const rendered = ReactTestRenderer.create(<ClassComponent />);
109+
const instance = rendered.getInstance();
110+
instance.setState(state => {
111+
setStateCount++;
112+
return {
113+
count: state.count + 1,
114+
};
115+
});
116+
117+
// Callback should be invoked twice
118+
expect(setStateCount).toBe(2);
119+
// But each time `state` should be the previous value
120+
expect(instance.state.count).toBe(2);
121+
});
122+
});
123+
});

packages/shared/ReactFeatureFlags.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@ export const enableNoopReconciler = false;
2424
// Experimental persistent mode (CS):
2525
export const enablePersistentReconciler = false;
2626

27+
// Helps identify side effects in begin-phase lifecycle hooks and setState reducers:
28+
export const debugRenderPhaseSideEffects = false;
29+
2730
// Only used in www builds.
2831
export function addUserTimingListener() {
2932
invariant(false, 'Not implemented.');

scripts/rollup/shims/rollup/ReactFeatureFlags-www.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ export const {
1616
} = require('ReactFeatureFlags');
1717

1818
// The rest of the flags are static for better dead code elimination.
19+
export const debugRenderPhaseSideEffects = false;
1920
export const enableAsyncSubtreeAPI = true;
2021
export const enableReactFragment = false;
2122
export const enableCreateRoot = true;

0 commit comments

Comments
 (0)