Skip to content

Commit 801f760

Browse files
committed
Handle UNSAFE_ lifecycle methods
1 parent 2b450d9 commit 801f760

File tree

6 files changed

+214
-12
lines changed

6 files changed

+214
-12
lines changed

src/components/LifecyclePanel.jsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import * as constants from '../constants';
55

66
const LifecyclePanel = (props) => {
77
const {componentName, isLegacy, instanceId, highlightedMethod, implementedMethods} = props;
8-
const lifecycleMethodNames = isLegacy ? constants.lifecycleMethodNamesLegacy : constants.lifecycleMethodNames;
8+
const lifecycleMethodNames = isLegacy ? constants.lifecycleMethodNamesLegacyNoUnsafe : constants.lifecycleMethodNames;
99

1010
return (
1111
<div className='lifecycle-panel'>

src/constants.js

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,20 @@ export const MWillMount = 'componentWillMount';
1717
export const MWillReceiveProps = 'componentWillReceiveProps';
1818
export const MWillUpdate = 'componentWillUpdate';
1919

20+
export const MUnsafeWillMount = 'UNSAFE_componentWillMount';
21+
export const MUnsafeWillReceiveProps = 'UNSAFE_componentWillReceiveProps';
22+
export const MUnsafeWillUpdate = 'UNSAFE_componentWillUpdate';
23+
2024
const lifecycleMethods = [
2125
{isLegacy: false, isNew: false, name: MConstructor},
2226
{isLegacy: true, isNew: false, name: MWillMount},
27+
{isLegacy: true, isNew: false, name: MUnsafeWillMount},
2328
{isLegacy: false, isNew: true, name: MGetDerivedState},
2429
{isLegacy: true, isNew: false, name: MWillReceiveProps},
30+
{isLegacy: true, isNew: false, name: MUnsafeWillReceiveProps},
2531
{isLegacy: false, isNew: false, name: MShouldUpdate},
2632
{isLegacy: true, isNew: false, name: MWillUpdate},
33+
{isLegacy: true, isNew: false, name: MUnsafeWillUpdate},
2734
{isLegacy: false, isNew: false, name: MRender},
2835
{isLegacy: false, isNew: false, name: MDidMount},
2936
{isLegacy: false, isNew: true, name: MGetSnapshot},
@@ -35,8 +42,11 @@ const lifecycleMethods = [
3542
export const lifecycleMethodNames =
3643
lifecycleMethods.filter((mthd) => !mthd.isLegacy).map(({name}) => name);
3744

38-
export const lifecycleMethodNamesLegacy =
39-
lifecycleMethods.filter((mthd) => !mthd.isNew).map(({name}) => name);
45+
// We don't show UNSAFE_ methods in the panel, but just use the shorter old names.
46+
export const lifecycleMethodNamesLegacyNoUnsafe =
47+
lifecycleMethods.filter(
48+
(mthd) => !mthd.isNew && !mthd.name.startsWith('UNSAFE_')
49+
).map(({name}) => name);
4050

4151
export const lifecycleMethodNamesNewOnly =
4252
lifecycleMethods.filter((mthd) => mthd.isNew).map(({name}) => name);

src/traceLifecycle.jsx

Lines changed: 46 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,9 @@ import * as ActionCreators from './redux/actionCreators';
77
import { withDeprecationWarning } from './util';
88
import LifecyclePanel from './components/LifecyclePanel';
99
import { MConstructor, MShouldUpdate, MRender, MDidMount,
10-
MDidUpdate, MWillUnmount, MSetState, MGetDerivedState,
11-
MGetSnapshot, MWillMount, MWillReceiveProps, MWillUpdate } from './constants';
10+
MDidUpdate, MWillUnmount, MSetState, MGetDerivedState, MGetSnapshot,
11+
MWillMount, MWillReceiveProps, MWillUpdate,
12+
MUnsafeWillMount, MUnsafeWillReceiveProps, MUnsafeWillUpdate} from './constants';
1213

1314
const instanceIdCounters = {};
1415

@@ -67,6 +68,13 @@ export default function traceLifecycle(ComponentToTrace) {
6768
}
6869
}
6970

71+
UNSAFE_componentWillMount() { // eslint-disable-line camelcase
72+
this.props.trace(MWillMount); // trace it as 'componentWillMount' for brevity
73+
if (super.UNSAFE_componentWillMount) {
74+
super.UNSAFE_componentWillMount();
75+
}
76+
}
77+
7078
static getDerivedStateFromProps(nextProps, prevState) {
7179
nextProps.trace(MGetDerivedState);
7280
return ComponentToTrace.getDerivedStateFromProps
@@ -95,6 +103,13 @@ export default function traceLifecycle(ComponentToTrace) {
95103
}
96104
}
97105

106+
UNSAFE_componentWillReceiveProps(...args) { // eslint-disable-line camelcase
107+
this.props.trace(MWillReceiveProps); // trace it as 'componentWillReceiveProps' for brevity
108+
if (super.UNSAFE_componentWillReceiveProps) {
109+
super.UNSAFE_componentWillReceiveProps(...args);
110+
}
111+
}
112+
98113
shouldComponentUpdate(...args) {
99114
this.props.trace(MShouldUpdate);
100115
return super.shouldComponentUpdate
@@ -109,12 +124,19 @@ export default function traceLifecycle(ComponentToTrace) {
109124
}
110125
}
111126

127+
UNSAFE_componentWillUpdate(...args) { // eslint-disable-line camelcase
128+
this.props.trace(MWillUpdate); // trace it as 'componentWillUpdate' for brevity
129+
if (super.UNSAFE_componentWillUpdate) {
130+
super.UNSAFE_componentWillUpdate(...args);
131+
}
132+
}
133+
112134
render() {
113135
if (super.render) {
114136
this.props.trace(MRender);
115137
return super.render();
116138
}
117-
return undefined; // no super.render, this will trigger a React error
139+
return undefined; // There's no super.render, which will trigger a React error
118140
}
119141

120142
getSnapshotBeforeUpdate(...args) {
@@ -134,8 +156,7 @@ export default function traceLifecycle(ComponentToTrace) {
134156
setState(updater, callback) {
135157
this.props.trace(MSetState);
136158

137-
// Unlike the lifecycle methods we only trace the update function and callback
138-
// when they are actually defined.
159+
// Unlike the lifecycle methods we only trace the update function and callback when they are actually defined.
139160
const tracingUpdater = typeof updater !== 'function' ? updater : (...args) => {
140161
this.props.trace(MSetState + ':update fn');
141162
return updater(...args);
@@ -184,18 +205,36 @@ export default function traceLifecycle(ComponentToTrace) {
184205
[constants.reduxStoreKey]: PropTypes.object
185206
}
186207

187-
static displayName =
188-
`traceLifecycle(${componentToTraceName})`;
208+
static displayName = `traceLifecycle(${componentToTraceName})`;
189209
}
190210

191211
// Removing the inappropriate methods is simpler than adding appropriate methods to prototype
192212
if (isLegacy) {
193213
delete TracedComponent.getDerivedStateFromProps;
194214
delete TracedComponent.prototype.getSnapshotBeforeUpdate;
215+
216+
// Only keep the tracer method corresponding to the implemented super method, unless neither the old or the
217+
// UNSAFE_ method is implemented, in which case we keep the UNSAFE_ method.
218+
// NOTE: This allows both the old method and the UNSAFE_ version to be traced, but this is correct, as React calls
219+
// both.
220+
const deleteOldOrUnsafe = (method, unsafeMethod) => {
221+
if (!superMethods.includes(method)) {
222+
delete TracedComponent.prototype[method];
223+
} else if (!superMethods.includes(unsafeMethod)) {
224+
delete TracedComponent.prototype[unsafeMethod];
225+
}
226+
};
227+
228+
deleteOldOrUnsafe(MWillMount, MUnsafeWillMount);
229+
deleteOldOrUnsafe(MWillReceiveProps, MUnsafeWillReceiveProps);
230+
deleteOldOrUnsafe(MWillUpdate, MUnsafeWillUpdate);
195231
} else {
196232
delete TracedComponent.prototype.componentWillMount;
197233
delete TracedComponent.prototype.componentWillReceiveProps;
198234
delete TracedComponent.prototype.componentWillUpdate;
235+
delete TracedComponent.prototype.UNSAFE_componentWillMount;
236+
delete TracedComponent.prototype.UNSAFE_componentWillReceiveProps;
237+
delete TracedComponent.prototype.UNSAFE_componentWillUpdate;
199238
}
200239

201240
return hoistStatics(TracingComponent, ComponentToTrace);

test/TracedLegacyUnsafeChild.jsx

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
/* eslint camelcase: 0 */
2+
import { Component } from 'react';
3+
import { traceLifecycle } from '../src';
4+
5+
class LegacyUnsafeChild extends Component {
6+
UNSAFE_componentWillMount() {
7+
this.props.trace('custom:UNSAFE_componentWillMount');
8+
}
9+
10+
UNSAFE_componentWillReceiveProps() {
11+
this.props.trace('custom:UNSAFE_componentWillReceiveProps');
12+
}
13+
14+
UNSAFE_componentWillUpdate() {
15+
this.props.trace('custom:UNSAFE_componentWillUpdate');
16+
}
17+
18+
render() {
19+
return '';
20+
}
21+
}
22+
23+
const TracedLegacyUnsafeChild = traceLifecycle(LegacyUnsafeChild);
24+
25+
export default TracedLegacyUnsafeChild;

test/integration.test.jsx

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { clearLog, Log, resetInstanceIdCounters, VisualizerProvider } from '../s
55

66
import TracedChild from './TracedChild';
77
import TracedLegacyChild from './TracedLegacyChild';
8+
import TracedLegacyUnsafeChild from './TracedLegacyUnsafeChild';
89

910
const nNewLifecyclePanelMethods = 9; // Non-legacy panel has 9 lifecycle methods
1011
const nLegacyLifecyclePanelMethods = 10; // Legacy panel has 10 lifecycle methods
@@ -14,8 +15,9 @@ const booleanListOnlyTrueAt = (n, i) => Array.from({length: n}, (_undefined, ix)
1415

1516
class Wrapper extends Component {
1617
state = {
17-
isShowingChild: false, // For mounting/unmounting TracedChild
18-
isShowingLegacyChild: false, // For mounting/unmounting TracedLegacyChild
18+
isShowingChild: false, // For mounting/unmounting TracedChild
19+
isShowingLegacyChild: false, // For mounting/unmounting TracedLegacyChild
20+
isShowingLegacyUnsafeChild: false, // For mounting/unmounting TracedLegacyUnsafeChild
1921
legacyProp: 0 // For updating props on TracedLegacyChild
2022
}
2123

@@ -25,6 +27,7 @@ class Wrapper extends Component {
2527
<div>
2628
{ this.state.isShowingChild && <TracedChild/> }
2729
{ this.state.isShowingLegacyChild && <TracedLegacyChild prop={this.state.legacyProp}/> }
30+
{ this.state.isShowingLegacyUnsafeChild && <TracedLegacyUnsafeChild prop={this.state.legacyProp}/> }
2831
<Log/>
2932
</div>
3033
</VisualizerProvider>
@@ -208,6 +211,36 @@ describe('Log', () => {
208211
);
209212
});
210213

214+
it('logs all legacy UNSAFE_ lifecycle methods', () => {
215+
wrapper.setState({isShowingLegacyUnsafeChild: true}); // Mount TracedLegacyUnsafeChild
216+
wrapper.setState({legacyProp: 42}); // Update TracedLegacyUnsafeChild props
217+
218+
jest.runAllTimers();
219+
wrapper.update();
220+
221+
const expectedLogEntries = [
222+
// Mount TracedLegacyUnsafeChild
223+
'constructor',
224+
'componentWillMount',
225+
'custom:UNSAFE_componentWillMount',
226+
'render',
227+
'componentDidMount',
228+
229+
// Update TracedLegacyUnsafeChild props
230+
'componentWillReceiveProps',
231+
'custom:UNSAFE_componentWillReceiveProps',
232+
'shouldComponentUpdate',
233+
'componentWillUpdate',
234+
'custom:UNSAFE_componentWillUpdate',
235+
'render',
236+
'componentDidUpdate',
237+
];
238+
239+
expect(wrapper.find('.entry').map((node) => node.text()))
240+
.toEqual(formatLogEntries('LegacyUnsafeChild-1', expectedLogEntries)
241+
);
242+
});
243+
211244
it('is cleared by clearLog()', () => {
212245
wrapper.setState({isShowingChild: true}); // Mount TracedChild
213246
jest.runAllTimers();

test/unsafe.test.jsx

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
/* eslint camelcase: 0, react/no-multi-comp: 0 */
2+
import React, { Component } from 'react';
3+
import { mount } from 'enzyme';
4+
import { traceLifecycle, VisualizerProvider } from '../src';
5+
6+
describe('unsafe', () => {
7+
it('Traces old methods if only old methods are defined', () => {
8+
const Comp = traceLifecycle(class Unsafe extends Component {
9+
componentWillMount() {}
10+
componentWillReceiveProps() {}
11+
componentWillUpdate() {}
12+
render() {
13+
return '';
14+
}
15+
});
16+
17+
const tracedInstance = mount(<VisualizerProvider><Comp/></VisualizerProvider>).find('TracedComponent').instance();
18+
expect(tracedInstance).toHaveProperty('componentWillMount');
19+
expect(tracedInstance).toHaveProperty('componentWillReceiveProps');
20+
expect(tracedInstance).toHaveProperty('componentWillUpdate');
21+
expect(tracedInstance).not.toHaveProperty('UNSAFE_componentWillMount');
22+
expect(tracedInstance).not.toHaveProperty('UNSAFE_componentWillReceiveProps');
23+
expect(tracedInstance).not.toHaveProperty('UNSAFE_componentWillUpdate');
24+
});
25+
26+
it('Traces UNSAFE_ methods if only UNSAFE_ methods are defined', () => {
27+
const Comp = traceLifecycle(class Unsafe extends Component {
28+
UNSAFE_componentWillMount() {}
29+
UNSAFE_componentWillReceiveProps() {}
30+
UNSAFE_componentWillUpdate() {}
31+
render() {
32+
return '';
33+
}
34+
});
35+
36+
const tracedInstance = mount(<VisualizerProvider><Comp/></VisualizerProvider>).find('TracedComponent').instance();
37+
expect(tracedInstance).not.toHaveProperty('componentWillMount');
38+
expect(tracedInstance).not.toHaveProperty('componentWillReceiveProps');
39+
expect(tracedInstance).not.toHaveProperty('componentWillUpdate');
40+
expect(tracedInstance).toHaveProperty('UNSAFE_componentWillMount');
41+
expect(tracedInstance).toHaveProperty('UNSAFE_componentWillReceiveProps');
42+
expect(tracedInstance).toHaveProperty('UNSAFE_componentWillUpdate');
43+
});
44+
45+
46+
it('Traces UNSAFE_ methods if neither old nor UNSAFE_ methods are defined (1)', () => {
47+
// Need two tests, since we need to define at least one UNSAFE_ method to turn it into a legacy component.
48+
const Comp = traceLifecycle(class Unsafe extends Component {
49+
UNSAFE_componentWillMount() {}
50+
render() {
51+
return '';
52+
}
53+
});
54+
const tracedInstance = mount(<VisualizerProvider><Comp/></VisualizerProvider>).find('TracedComponent').instance();
55+
expect(tracedInstance).not.toHaveProperty('componentWillReceiveProps');
56+
expect(tracedInstance).not.toHaveProperty('componentWillUpdate');
57+
expect(tracedInstance).toHaveProperty('UNSAFE_componentWillReceiveProps');
58+
expect(tracedInstance).toHaveProperty('UNSAFE_componentWillUpdate');
59+
});
60+
61+
it('Traces UNSAFE_ methods if neither old nor UNSAFE_ methods are defined (2)', () => {
62+
const Comp = traceLifecycle(class Unsafe extends Component {
63+
UNSAFE_componentWillReceiveProps() {}
64+
render() {
65+
return '';
66+
}
67+
});
68+
const tracedInstance = mount(<VisualizerProvider><Comp/></VisualizerProvider>).find('TracedComponent').instance();
69+
expect(tracedInstance).not.toHaveProperty('componentWillMount');
70+
expect(tracedInstance).toHaveProperty('UNSAFE_componentWillMount');
71+
});
72+
73+
it('Traces both old and UNSAFE_ methods if both are defined', () => {
74+
// Kind of silly, but this is allwed by React.
75+
const Comp = traceLifecycle(class Unsafe extends Component {
76+
UNSAFE_componentWillMount() {}
77+
UNSAFE_componentWillReceiveProps() {}
78+
UNSAFE_componentWillUpdate() {}
79+
componentWillMount() {}
80+
componentWillReceiveProps() {}
81+
componentWillUpdate() {}
82+
render() {
83+
return '';
84+
}
85+
});
86+
87+
const tracedInstance = mount(<VisualizerProvider><Comp/></VisualizerProvider>).find('TracedComponent').instance();
88+
expect(tracedInstance).toHaveProperty('componentWillMount');
89+
expect(tracedInstance).toHaveProperty('componentWillReceiveProps');
90+
expect(tracedInstance).toHaveProperty('componentWillUpdate');
91+
expect(tracedInstance).toHaveProperty('UNSAFE_componentWillMount');
92+
expect(tracedInstance).toHaveProperty('UNSAFE_componentWillReceiveProps');
93+
expect(tracedInstance).toHaveProperty('UNSAFE_componentWillUpdate');
94+
});
95+
});

0 commit comments

Comments
 (0)