Skip to content

Commit 8e3532a

Browse files
authored
React lifecycles compat (#12105)
* Suppress unsafe/deprecation warnings for polyfilled components. * Don't invoke deprecated lifecycles if static gDSFP exists. * Applied recent changes to server rendering also
1 parent 4a38d6d commit 8e3532a

3 files changed

+205
-44
lines changed

src/ReactShallowRenderer.js

+37-8
Original file line numberDiff line numberDiff line change
@@ -180,7 +180,12 @@ class ReactShallowRenderer {
180180

181181
if (typeof this._instance.componentWillMount === 'function') {
182182
if (__DEV__) {
183-
if (warnAboutDeprecatedLifecycles) {
183+
// Don't warn about react-lifecycles-compat polyfilled components
184+
if (
185+
warnAboutDeprecatedLifecycles &&
186+
this._instance.componentWillMount.__suppressDeprecationWarning !==
187+
true
188+
) {
184189
const componentName = getName(element.type, this._instance);
185190
if (!didWarnAboutLegacyWillMount[componentName]) {
186191
warning(
@@ -198,8 +203,15 @@ class ReactShallowRenderer {
198203
}
199204
}
200205
}
201-
this._instance.componentWillMount();
202-
} else {
206+
207+
// In order to support react-lifecycles-compat polyfilled components,
208+
// Unsafe lifecycles should not be invoked for any component with the new gDSFP.
209+
if (typeof element.type.getDerivedStateFromProps !== 'function') {
210+
this._instance.componentWillMount();
211+
}
212+
} else if (typeof element.type.getDerivedStateFromProps !== 'function') {
213+
// In order to support react-lifecycles-compat polyfilled components,
214+
// Unsafe lifecycles should not be invoked for any component with the new gDSFP.
203215
this._instance.UNSAFE_componentWillMount();
204216
}
205217

@@ -242,10 +254,17 @@ class ReactShallowRenderer {
242254
}
243255
}
244256
}
245-
this._instance.componentWillReceiveProps(props, context);
257+
// In order to support react-lifecycles-compat polyfilled components,
258+
// Unsafe lifecycles should not be invoked for any component with the new gDSFP.
259+
if (typeof element.type.getDerivedStateFromProps !== 'function') {
260+
this._instance.componentWillReceiveProps(props, context);
261+
}
246262
} else if (
247-
typeof this._instance.UNSAFE_componentWillReceiveProps === 'function'
263+
typeof this._instance.UNSAFE_componentWillReceiveProps === 'function' &&
264+
typeof element.type.getDerivedStateFromProps !== 'function'
248265
) {
266+
// In order to support react-lifecycles-compat polyfilled components,
267+
// Unsafe lifecycles should not be invoked for any component with the new gDSFP.
249268
this._instance.UNSAFE_componentWillReceiveProps(props, context);
250269
}
251270

@@ -292,10 +311,17 @@ class ReactShallowRenderer {
292311
}
293312
}
294313

295-
this._instance.componentWillUpdate(props, state, context);
314+
// In order to support react-lifecycles-compat polyfilled components,
315+
// Unsafe lifecycles should not be invoked for any component with the new gDSFP.
316+
if (typeof type.getDerivedStateFromProps !== 'function') {
317+
this._instance.componentWillUpdate(props, state, context);
318+
}
296319
} else if (
297-
typeof this._instance.UNSAFE_componentWillUpdate === 'function'
320+
typeof this._instance.UNSAFE_componentWillUpdate === 'function' &&
321+
typeof type.getDerivedStateFromProps !== 'function'
298322
) {
323+
// In order to support react-lifecycles-compat polyfilled components,
324+
// Unsafe lifecycles should not be invoked for any component with the new gDSFP.
299325
this._instance.UNSAFE_componentWillUpdate(props, state, context);
300326
}
301327
}
@@ -316,8 +342,11 @@ class ReactShallowRenderer {
316342

317343
if (typeof type.getDerivedStateFromProps === 'function') {
318344
if (__DEV__) {
345+
// Don't warn about react-lifecycles-compat polyfilled components
319346
if (
320-
typeof this._instance.componentWillReceiveProps === 'function' ||
347+
(typeof this._instance.componentWillReceiveProps === 'function' &&
348+
this._instance.componentWillReceiveProps
349+
.__suppressDeprecationWarning !== true) ||
321350
typeof this._instance.UNSAFE_componentWillReceiveProps === 'function'
322351
) {
323352
const componentName = getName(type, this._instance);

src/__tests__/ReactShallowRenderer-test.internal.js

+31
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,10 @@ describe('ReactShallowRenderer', () => {
2323
React = require('react');
2424
});
2525

26+
afterEach(() => {
27+
jest.resetModules();
28+
});
29+
2630
// TODO (RFC #6) Merge this back into ReactShallowRenderer-test once
2731
// the 'warnAboutDeprecatedLifecycles' feature flag has been removed.
2832
it('should warn if deprecated lifecycles exist', () => {
@@ -50,4 +54,31 @@ describe('ReactShallowRenderer', () => {
5054
// Verify no duplicate warnings
5155
shallowRenderer.render(<ComponentWithWarnings />);
5256
});
57+
58+
describe('react-lifecycles-compat', () => {
59+
// TODO Replace this with react-lifecycles-compat once it's been published
60+
function polyfill(Component) {
61+
Component.prototype.componentWillMount = function() {};
62+
Component.prototype.componentWillMount.__suppressDeprecationWarning = true;
63+
Component.prototype.componentWillReceiveProps = function() {};
64+
Component.prototype.componentWillReceiveProps.__suppressDeprecationWarning = true;
65+
}
66+
67+
it('should not warn about deprecated cWM/cWRP for polyfilled components', () => {
68+
class PolyfilledComponent extends React.Component {
69+
state = {};
70+
static getDerivedStateFromProps() {
71+
return null;
72+
}
73+
render() {
74+
return null;
75+
}
76+
}
77+
78+
polyfill(PolyfilledComponent);
79+
80+
const shallowRenderer = createRenderer();
81+
shallowRenderer.render(<PolyfilledComponent />);
82+
});
83+
});
5384
});

src/__tests__/ReactShallowRenderer-test.js

+137-36
Original file line numberDiff line numberDiff line change
@@ -23,13 +23,11 @@ describe('ReactShallowRenderer', () => {
2323
React = require('react');
2424
});
2525

26-
it('should call all of the lifecycle hooks', () => {
26+
it('should call all of the legacy lifecycle hooks', () => {
2727
const logs = [];
2828
const logger = message => () => logs.push(message) || true;
2929

3030
class SomeComponent extends React.Component {
31-
state = {};
32-
static getDerivedStateFromProps = logger('getDerivedStateFromProps');
3331
UNSAFE_componentWillMount = logger('componentWillMount');
3432
componentDidMount = logger('componentDidMount');
3533
UNSAFE_componentWillReceiveProps = logger('componentWillReceiveProps');
@@ -43,16 +41,11 @@ describe('ReactShallowRenderer', () => {
4341
}
4442

4543
const shallowRenderer = createRenderer();
46-
47-
expect(() => shallowRenderer.render(<SomeComponent foo={1} />)).toWarnDev(
48-
'Warning: SomeComponent: Defines both componentWillReceiveProps() and static ' +
49-
'getDerivedStateFromProps() methods. ' +
50-
'We recommend using only getDerivedStateFromProps().',
51-
);
44+
shallowRenderer.render(<SomeComponent foo={1} />);
5245

5346
// Calling cDU might lead to problems with host component references.
5447
// Since our components aren't really mounted, refs won't be available.
55-
expect(logs).toEqual(['getDerivedStateFromProps', 'componentWillMount']);
48+
expect(logs).toEqual(['componentWillMount']);
5649

5750
logs.splice(0);
5851

@@ -68,12 +61,75 @@ describe('ReactShallowRenderer', () => {
6861
// The previous shallow renderer did not trigger cDU for props changes.
6962
expect(logs).toEqual([
7063
'componentWillReceiveProps',
71-
'getDerivedStateFromProps',
7264
'shouldComponentUpdate',
7365
'componentWillUpdate',
7466
]);
7567
});
7668

69+
it('should call all of the new lifecycle hooks', () => {
70+
const logs = [];
71+
const logger = message => () => logs.push(message) || true;
72+
73+
class SomeComponent extends React.Component {
74+
state = {};
75+
static getDerivedStateFromProps = logger('getDerivedStateFromProps');
76+
componentDidMount = logger('componentDidMount');
77+
shouldComponentUpdate = logger('shouldComponentUpdate');
78+
componentDidUpdate = logger('componentDidUpdate');
79+
componentWillUnmount = logger('componentWillUnmount');
80+
render() {
81+
return <div />;
82+
}
83+
}
84+
85+
const shallowRenderer = createRenderer();
86+
shallowRenderer.render(<SomeComponent foo={1} />);
87+
88+
// Calling cDU might lead to problems with host component references.
89+
// Since our components aren't really mounted, refs won't be available.
90+
expect(logs).toEqual(['getDerivedStateFromProps']);
91+
92+
logs.splice(0);
93+
94+
const instance = shallowRenderer.getMountedInstance();
95+
instance.setState({});
96+
97+
expect(logs).toEqual(['shouldComponentUpdate']);
98+
99+
logs.splice(0);
100+
101+
shallowRenderer.render(<SomeComponent foo={2} />);
102+
103+
// The previous shallow renderer did not trigger cDU for props changes.
104+
expect(logs).toEqual(['getDerivedStateFromProps', 'shouldComponentUpdate']);
105+
});
106+
107+
it('should not invoke deprecated lifecycles (cWM/cWRP/cWU) if new static gDSFP is present', () => {
108+
class Component extends React.Component {
109+
state = {};
110+
static getDerivedStateFromProps() {
111+
return null;
112+
}
113+
componentWillMount() {
114+
throw Error('unexpected');
115+
}
116+
componentWillReceiveProps() {
117+
throw Error('unexpected');
118+
}
119+
componentWillUpdate() {
120+
throw Error('unexpected');
121+
}
122+
render() {
123+
return null;
124+
}
125+
}
126+
127+
const shallowRenderer = createRenderer();
128+
expect(() => shallowRenderer.render(<Component foo={2} />)).toWarnDev(
129+
'Defines both componentWillReceiveProps() and static getDerivedStateFromProps()',
130+
);
131+
});
132+
77133
it('should only render 1 level deep', () => {
78134
function Parent() {
79135
return (
@@ -422,11 +478,10 @@ describe('ReactShallowRenderer', () => {
422478
expect(result).toEqual(<div />);
423479
});
424480

425-
it('passes expected params to component lifecycle methods', () => {
481+
it('passes expected params to legacy component lifecycle methods', () => {
426482
const componentDidUpdateParams = [];
427483
const componentWillReceivePropsParams = [];
428484
const componentWillUpdateParams = [];
429-
const getDerivedStateFromPropsParams = [];
430485
const setStateParams = [];
431486
const shouldComponentUpdateParams = [];
432487

@@ -448,10 +503,6 @@ describe('ReactShallowRenderer', () => {
448503
componentDidUpdate(...args) {
449504
componentDidUpdateParams.push(...args);
450505
}
451-
static getDerivedStateFromProps(...args) {
452-
getDerivedStateFromPropsParams.push(args);
453-
return null;
454-
}
455506
UNSAFE_componentWillReceiveProps(...args) {
456507
componentWillReceivePropsParams.push(...args);
457508
this.setState((...innerArgs) => {
@@ -472,22 +523,10 @@ describe('ReactShallowRenderer', () => {
472523
}
473524

474525
const shallowRenderer = createRenderer();
475-
476-
// The only lifecycle hook that should be invoked on initial render
477-
// Is the static getDerivedStateFromProps() methods
478-
expect(() =>
479-
shallowRenderer.render(
480-
React.createElement(SimpleComponent, initialProp),
481-
initialContext,
482-
),
483-
).toWarnDev(
484-
'SimpleComponent: Defines both componentWillReceiveProps() and static ' +
485-
'getDerivedStateFromProps() methods. We recommend using ' +
486-
'only getDerivedStateFromProps().',
526+
shallowRenderer.render(
527+
React.createElement(SimpleComponent, initialProp),
528+
initialContext,
487529
);
488-
expect(getDerivedStateFromPropsParams).toEqual([
489-
[initialProp, initialState],
490-
]);
491530
expect(componentDidUpdateParams).toEqual([]);
492531
expect(componentWillReceivePropsParams).toEqual([]);
493532
expect(componentWillUpdateParams).toEqual([]);
@@ -504,10 +543,6 @@ describe('ReactShallowRenderer', () => {
504543
updatedContext,
505544
]);
506545
expect(setStateParams).toEqual([initialState, initialProp]);
507-
expect(getDerivedStateFromPropsParams).toEqual([
508-
[initialProp, initialState],
509-
[updatedProp, initialState],
510-
]);
511546
expect(shouldComponentUpdateParams).toEqual([
512547
updatedProp,
513548
updatedState,
@@ -521,6 +556,72 @@ describe('ReactShallowRenderer', () => {
521556
expect(componentDidUpdateParams).toEqual([]);
522557
});
523558

559+
it('passes expected params to new component lifecycle methods', () => {
560+
const componentDidUpdateParams = [];
561+
const getDerivedStateFromPropsParams = [];
562+
const shouldComponentUpdateParams = [];
563+
564+
const initialProp = {prop: 'init prop'};
565+
const initialState = {state: 'init state'};
566+
const initialContext = {context: 'init context'};
567+
const updatedProp = {prop: 'updated prop'};
568+
const updatedContext = {context: 'updated context'};
569+
570+
class SimpleComponent extends React.Component {
571+
constructor(props, context) {
572+
super(props, context);
573+
this.state = initialState;
574+
}
575+
static contextTypes = {
576+
context: PropTypes.string,
577+
};
578+
componentDidUpdate(...args) {
579+
componentDidUpdateParams.push(...args);
580+
}
581+
static getDerivedStateFromProps(...args) {
582+
getDerivedStateFromPropsParams.push(args);
583+
return null;
584+
}
585+
shouldComponentUpdate(...args) {
586+
shouldComponentUpdateParams.push(...args);
587+
return true;
588+
}
589+
render() {
590+
return null;
591+
}
592+
}
593+
594+
const shallowRenderer = createRenderer();
595+
596+
// The only lifecycle hook that should be invoked on initial render
597+
// Is the static getDerivedStateFromProps() methods
598+
shallowRenderer.render(
599+
React.createElement(SimpleComponent, initialProp),
600+
initialContext,
601+
);
602+
expect(getDerivedStateFromPropsParams).toEqual([
603+
[initialProp, initialState],
604+
]);
605+
expect(componentDidUpdateParams).toEqual([]);
606+
expect(shouldComponentUpdateParams).toEqual([]);
607+
608+
// Lifecycle hooks should be invoked with the correct prev/next params on update.
609+
shallowRenderer.render(
610+
React.createElement(SimpleComponent, updatedProp),
611+
updatedContext,
612+
);
613+
expect(getDerivedStateFromPropsParams).toEqual([
614+
[initialProp, initialState],
615+
[updatedProp, initialState],
616+
]);
617+
expect(shouldComponentUpdateParams).toEqual([
618+
updatedProp,
619+
initialState,
620+
updatedContext,
621+
]);
622+
expect(componentDidUpdateParams).toEqual([]);
623+
});
624+
524625
it('can shallowly render components with ref as function', () => {
525626
class SimpleComponent extends React.Component {
526627
state = {clicked: false};

0 commit comments

Comments
 (0)