Skip to content

Commit 482f6ef

Browse files
authored
RFC 6: Deprecate unsafe lifecycles (#12028)
* Added unsafe_* lifecycles and deprecation warnings If the old lifecycle hooks (componentWillMount, componentWillUpdate, componentWillReceiveProps) are detected, these methods will be called and a deprecation warning will be logged. (In other words, we do not check for both the presence of the old and new lifecycles.) This commit is expected to fail tests. * Ran lifecycle hook codemod over project This should handle the bulk of the updates. I will manually update TypeScript and CoffeeScript tests with another commit. The actual command run with this commit was: jscodeshift --parser=flow -t ../react-codemod/transforms/rename-unsafe-lifecycles.js ./packages/**/src/**/*.js * Manually migrated CoffeeScript and TypeScript tests * Added inline note to createReactClassIntegration-test Explaining why lifecycles hooks have not been renamed in this test. * Udated NativeMethodsMixin with new lifecycle hooks * Added static getDerivedStateFromProps to ReactPartialRenderer Also added a new set of tests focused on server side lifecycle hooks. * Added getDerivedStateFromProps to shallow renderer Also added warnings for several cases involving getDerivedStateFromProps() as well as the deprecated lifecycles. Also added tests for the above. * Dedupe and DEV-only deprecation warning in server renderer * Renamed unsafe_* prefix to UNSAFE_* to be more noticeable * Added getDerivedStateFromProps to ReactFiberClassComponent Also updated class component and lifecyle tests to cover the added functionality. * Warn about UNSAFE_componentWillRecieveProps misspelling * Added tests to createReactClassIntegration for new lifecycles * Added warning for stateless functional components with gDSFP * Added createReactClass test for static gDSFP * Moved lifecycle deprecation warnings behind (disabled) feature flag Updated tests accordingly, by temporarily splitting tests that were specific to this feature-flag into their own, internal tests. This was the only way I knew of to interact with the feature flag without breaking our build/dist tests. * Tidying up * Tweaked warning message wording slightly Replaced 'You may may have returned undefined.' with 'You may have returned undefined.' * Replaced truthy partialState checks with != null * Call getDerivedStateFromProps via .call(null) to prevent type access * Move shallow-renderer didWarn* maps off the instance * Only call getDerivedStateFromProps if props instance has changed * Avoid creating new state object if not necessary * Inject state as a param to callGetDerivedStateFromProps This value will be either workInProgress.memoizedState (for updates) or instance.state (for initialization). * Explicitly warn about uninitialized state before calling getDerivedStateFromProps. And added some new tests for this change. Also: * Improved a couple of falsy null/undefined checks to more explicitly check for null or undefined. * Made some small tweaks to ReactFiberClassComponent WRT when and how it reads instance.state and sets to null. * Improved wording for deprecation lifecycle warnings * Fix state-regression for module-pattern components Also add support for new static getDerivedStateFromProps method
1 parent ac7096e commit 482f6ef

3 files changed

+406
-29
lines changed

src/ReactShallowRenderer.js

+186-12
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,32 @@
77
*/
88

99
import React from 'react';
10+
import {warnAboutDeprecatedLifecycles} from 'shared/ReactFeatureFlags';
1011
import describeComponentFrame from 'shared/describeComponentFrame';
1112
import getComponentName from 'shared/getComponentName';
1213
import emptyObject from 'fbjs/lib/emptyObject';
1314
import invariant from 'fbjs/lib/invariant';
1415
import shallowEqual from 'fbjs/lib/shallowEqual';
1516
import checkPropTypes from 'prop-types/checkPropTypes';
17+
import warning from 'fbjs/lib/warning';
18+
19+
let didWarnAboutLegacyWillMount;
20+
let didWarnAboutLegacyWillReceiveProps;
21+
let didWarnAboutLegacyWillUpdate;
22+
let didWarnAboutUndefinedDerivedState;
23+
let didWarnAboutUninitializedState;
24+
let didWarnAboutWillReceivePropsAndDerivedState;
25+
26+
if (__DEV__) {
27+
if (warnAboutDeprecatedLifecycles) {
28+
didWarnAboutLegacyWillMount = {};
29+
didWarnAboutLegacyWillReceiveProps = {};
30+
didWarnAboutLegacyWillUpdate = {};
31+
}
32+
didWarnAboutUndefinedDerivedState = {};
33+
didWarnAboutUninitializedState = {};
34+
didWarnAboutWillReceivePropsAndDerivedState = {};
35+
}
1636

1737
class ReactShallowRenderer {
1838
static createRenderer = function() {
@@ -73,7 +93,7 @@ class ReactShallowRenderer {
7393
this._context = getMaskedContext(element.type.contextTypes, context);
7494

7595
if (this._instance) {
76-
this._updateClassComponent(element.type, element.props, this._context);
96+
this._updateClassComponent(element, this._context);
7797
} else {
7898
if (shouldConstruct(element.type)) {
7999
this._instance = new element.type(
@@ -82,6 +102,30 @@ class ReactShallowRenderer {
82102
this._updater,
83103
);
84104

105+
if (__DEV__) {
106+
if (typeof element.type.getDerivedStateFromProps === 'function') {
107+
if (
108+
this._instance.state === null ||
109+
this._instance.state === undefined
110+
) {
111+
const componentName =
112+
getName(element.type, this._instance) || 'Unknown';
113+
if (!didWarnAboutUninitializedState[componentName]) {
114+
warning(
115+
false,
116+
'%s: Did not properly initialize state during construction. ' +
117+
'Expected state to be an object, but it was %s.',
118+
componentName,
119+
this._instance.state === null ? 'null' : 'undefined',
120+
);
121+
didWarnAboutUninitializedState[componentName] = true;
122+
}
123+
}
124+
}
125+
}
126+
127+
this._updateStateFromStaticLifecycle(element.props);
128+
85129
if (element.type.hasOwnProperty('contextTypes')) {
86130
currentlyValidatingElement = element;
87131

@@ -96,7 +140,7 @@ class ReactShallowRenderer {
96140
currentlyValidatingElement = null;
97141
}
98142

99-
this._mountClassComponent(element.props, this._context);
143+
this._mountClassComponent(element, this._context);
100144
} else {
101145
this._rendered = element.type(element.props, this._context);
102146
}
@@ -122,16 +166,42 @@ class ReactShallowRenderer {
122166
this._instance = null;
123167
}
124168

125-
_mountClassComponent(props, context) {
169+
_mountClassComponent(element, context) {
126170
this._instance.context = context;
127-
this._instance.props = props;
171+
this._instance.props = element.props;
128172
this._instance.state = this._instance.state || null;
129173
this._instance.updater = this._updater;
130174

131-
if (typeof this._instance.componentWillMount === 'function') {
175+
if (
176+
typeof this._instance.UNSAFE_componentWillMount === 'function' ||
177+
typeof this._instance.componentWillMount === 'function'
178+
) {
132179
const beforeState = this._newState;
133180

134-
this._instance.componentWillMount();
181+
if (typeof this._instance.componentWillMount === 'function') {
182+
if (__DEV__) {
183+
if (warnAboutDeprecatedLifecycles) {
184+
const componentName = getName(element.type, this._instance);
185+
if (!didWarnAboutLegacyWillMount[componentName]) {
186+
warning(
187+
false,
188+
'%s: componentWillMount() is deprecated and will be ' +
189+
'removed in the next major version. Read about the motivations ' +
190+
'behind this change: ' +
191+
'https://fb.me/react-async-component-lifecycle-hooks' +
192+
'\n\n' +
193+
'As a temporary workaround, you can rename to ' +
194+
'UNSAFE_componentWillMount instead.',
195+
componentName,
196+
);
197+
didWarnAboutLegacyWillMount[componentName] = true;
198+
}
199+
}
200+
}
201+
this._instance.componentWillMount();
202+
} else {
203+
this._instance.UNSAFE_componentWillMount();
204+
}
135205

136206
// setState may have been called during cWM
137207
if (beforeState !== this._newState) {
@@ -144,16 +214,44 @@ class ReactShallowRenderer {
144214
// because DOM refs are not available.
145215
}
146216

147-
_updateClassComponent(type, props, context) {
217+
_updateClassComponent(element, context) {
218+
const {props, type} = element;
219+
148220
const oldState = this._instance.state || emptyObject;
149221
const oldProps = this._instance.props;
150222

151-
if (
152-
oldProps !== props &&
153-
typeof this._instance.componentWillReceiveProps === 'function'
154-
) {
155-
this._instance.componentWillReceiveProps(props, context);
223+
if (oldProps !== props) {
224+
if (typeof this._instance.componentWillReceiveProps === 'function') {
225+
if (__DEV__) {
226+
if (warnAboutDeprecatedLifecycles) {
227+
const componentName = getName(element.type, this._instance);
228+
if (!didWarnAboutLegacyWillReceiveProps[componentName]) {
229+
warning(
230+
false,
231+
'%s: componentWillReceiveProps() is deprecated and ' +
232+
'will be removed in the next major version. Use ' +
233+
'static getDerivedStateFromProps() instead. Read about the ' +
234+
'motivations behind this change: ' +
235+
'https://fb.me/react-async-component-lifecycle-hooks' +
236+
'\n\n' +
237+
'As a temporary workaround, you can rename to ' +
238+
'UNSAFE_componentWillReceiveProps instead.',
239+
componentName,
240+
);
241+
didWarnAboutLegacyWillReceiveProps[componentName] = true;
242+
}
243+
}
244+
}
245+
this._instance.componentWillReceiveProps(props, context);
246+
} else if (
247+
typeof this._instance.UNSAFE_componentWillReceiveProps === 'function'
248+
) {
249+
this._instance.UNSAFE_componentWillReceiveProps(props, context);
250+
}
251+
252+
this._updateStateFromStaticLifecycle(props);
156253
}
254+
157255
// Read state after cWRP in case it calls setState
158256
const state = this._newState || oldState;
159257

@@ -174,7 +272,31 @@ class ReactShallowRenderer {
174272

175273
if (shouldUpdate) {
176274
if (typeof this._instance.componentWillUpdate === 'function') {
275+
if (__DEV__) {
276+
if (warnAboutDeprecatedLifecycles) {
277+
const componentName = getName(element.type, this._instance);
278+
if (!didWarnAboutLegacyWillUpdate[componentName]) {
279+
warning(
280+
false,
281+
'%s: componentWillUpdate() is deprecated and will be ' +
282+
'removed in the next major version. Read about the motivations ' +
283+
'behind this change: ' +
284+
'https://fb.me/react-async-component-lifecycle-hooks' +
285+
'\n\n' +
286+
'As a temporary workaround, you can rename to ' +
287+
'UNSAFE_componentWillUpdate instead.',
288+
componentName,
289+
);
290+
didWarnAboutLegacyWillUpdate[componentName] = true;
291+
}
292+
}
293+
}
294+
177295
this._instance.componentWillUpdate(props, state, context);
296+
} else if (
297+
typeof this._instance.UNSAFE_componentWillUpdate === 'function'
298+
) {
299+
this._instance.UNSAFE_componentWillUpdate(props, state, context);
178300
}
179301
}
180302

@@ -188,6 +310,58 @@ class ReactShallowRenderer {
188310
// Intentionally do not call componentDidUpdate()
189311
// because DOM refs are not available.
190312
}
313+
314+
_updateStateFromStaticLifecycle(props) {
315+
const {type} = this._element;
316+
317+
if (typeof type.getDerivedStateFromProps === 'function') {
318+
if (__DEV__) {
319+
if (
320+
typeof this._instance.componentWillReceiveProps === 'function' ||
321+
typeof this._instance.UNSAFE_componentWillReceiveProps === 'function'
322+
) {
323+
const componentName = getName(type, this._instance);
324+
if (!didWarnAboutWillReceivePropsAndDerivedState[componentName]) {
325+
warning(
326+
false,
327+
'%s: Defines both componentWillReceiveProps() and static ' +
328+
'getDerivedStateFromProps() methods. We recommend using ' +
329+
'only getDerivedStateFromProps().',
330+
componentName,
331+
);
332+
didWarnAboutWillReceivePropsAndDerivedState[componentName] = true;
333+
}
334+
}
335+
}
336+
337+
const partialState = type.getDerivedStateFromProps.call(
338+
null,
339+
props,
340+
this._instance.state,
341+
);
342+
343+
if (__DEV__) {
344+
if (partialState === undefined) {
345+
const componentName = getName(type, this._instance);
346+
if (!didWarnAboutUndefinedDerivedState[componentName]) {
347+
warning(
348+
false,
349+
'%s.getDerivedStateFromProps(): A valid state object (or null) must be returned. ' +
350+
'You have returned undefined.',
351+
componentName,
352+
);
353+
didWarnAboutUndefinedDerivedState[componentName] = componentName;
354+
}
355+
}
356+
}
357+
358+
if (partialState != null) {
359+
const oldState = this._newState || this._instance.state;
360+
const newState = Object.assign({}, oldState, partialState);
361+
this._instance.state = this._newState = newState;
362+
}
363+
}
364+
}
191365
}
192366

193367
class Updater {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
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+
* @jest-environment node
9+
*/
10+
11+
'use strict';
12+
13+
let createRenderer;
14+
let React;
15+
let ReactFeatureFlags;
16+
17+
describe('ReactShallowRenderer', () => {
18+
beforeEach(() => {
19+
ReactFeatureFlags = require('shared/ReactFeatureFlags');
20+
ReactFeatureFlags.warnAboutDeprecatedLifecycles = true;
21+
22+
createRenderer = require('react-test-renderer/shallow').createRenderer;
23+
React = require('react');
24+
});
25+
26+
// TODO (RFC #6) Merge this back into ReactShallowRenderer-test once
27+
// the 'warnAboutDeprecatedLifecycles' feature flag has been removed.
28+
it('should warn if deprecated lifecycles exist', () => {
29+
class ComponentWithWarnings extends React.Component {
30+
componentWillReceiveProps() {}
31+
componentWillMount() {}
32+
componentWillUpdate() {}
33+
render() {
34+
return null;
35+
}
36+
}
37+
38+
const shallowRenderer = createRenderer();
39+
expect(() => shallowRenderer.render(<ComponentWithWarnings />)).toWarnDev(
40+
'Warning: ComponentWithWarnings: componentWillMount() is deprecated and will ' +
41+
'be removed in the next major version.',
42+
);
43+
expect(() => shallowRenderer.render(<ComponentWithWarnings />)).toWarnDev([
44+
'Warning: ComponentWithWarnings: componentWillReceiveProps() is deprecated ' +
45+
'and will be removed in the next major version.',
46+
'Warning: ComponentWithWarnings: componentWillUpdate() is deprecated and will ' +
47+
'be removed in the next major version.',
48+
]);
49+
50+
// Verify no duplicate warnings
51+
shallowRenderer.render(<ComponentWithWarnings />);
52+
});
53+
});

0 commit comments

Comments
 (0)