Skip to content

Commit

Permalink
RFC 6: Deprecate unsafe lifecycles (#12028)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
bvaughn committed Jan 19, 2018
1 parent ac7096e commit 482f6ef
Show file tree
Hide file tree
Showing 3 changed files with 406 additions and 29 deletions.
198 changes: 186 additions & 12 deletions src/ReactShallowRenderer.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,32 @@
*/

import React from 'react';
import {warnAboutDeprecatedLifecycles} from 'shared/ReactFeatureFlags';
import describeComponentFrame from 'shared/describeComponentFrame';
import getComponentName from 'shared/getComponentName';
import emptyObject from 'fbjs/lib/emptyObject';
import invariant from 'fbjs/lib/invariant';
import shallowEqual from 'fbjs/lib/shallowEqual';
import checkPropTypes from 'prop-types/checkPropTypes';
import warning from 'fbjs/lib/warning';

let didWarnAboutLegacyWillMount;
let didWarnAboutLegacyWillReceiveProps;
let didWarnAboutLegacyWillUpdate;
let didWarnAboutUndefinedDerivedState;
let didWarnAboutUninitializedState;
let didWarnAboutWillReceivePropsAndDerivedState;

if (__DEV__) {
if (warnAboutDeprecatedLifecycles) {
didWarnAboutLegacyWillMount = {};
didWarnAboutLegacyWillReceiveProps = {};
didWarnAboutLegacyWillUpdate = {};
}
didWarnAboutUndefinedDerivedState = {};
didWarnAboutUninitializedState = {};
didWarnAboutWillReceivePropsAndDerivedState = {};
}

class ReactShallowRenderer {
static createRenderer = function() {
Expand Down Expand Up @@ -73,7 +93,7 @@ class ReactShallowRenderer {
this._context = getMaskedContext(element.type.contextTypes, context);

if (this._instance) {
this._updateClassComponent(element.type, element.props, this._context);
this._updateClassComponent(element, this._context);
} else {
if (shouldConstruct(element.type)) {
this._instance = new element.type(
Expand All @@ -82,6 +102,30 @@ class ReactShallowRenderer {
this._updater,
);

if (__DEV__) {
if (typeof element.type.getDerivedStateFromProps === 'function') {
if (
this._instance.state === null ||
this._instance.state === undefined
) {
const componentName =
getName(element.type, this._instance) || 'Unknown';
if (!didWarnAboutUninitializedState[componentName]) {
warning(
false,
'%s: Did not properly initialize state during construction. ' +
'Expected state to be an object, but it was %s.',
componentName,
this._instance.state === null ? 'null' : 'undefined',
);
didWarnAboutUninitializedState[componentName] = true;
}
}
}
}

this._updateStateFromStaticLifecycle(element.props);

if (element.type.hasOwnProperty('contextTypes')) {
currentlyValidatingElement = element;

Expand All @@ -96,7 +140,7 @@ class ReactShallowRenderer {
currentlyValidatingElement = null;
}

this._mountClassComponent(element.props, this._context);
this._mountClassComponent(element, this._context);
} else {
this._rendered = element.type(element.props, this._context);
}
Expand All @@ -122,16 +166,42 @@ class ReactShallowRenderer {
this._instance = null;
}

_mountClassComponent(props, context) {
_mountClassComponent(element, context) {
this._instance.context = context;
this._instance.props = props;
this._instance.props = element.props;
this._instance.state = this._instance.state || null;
this._instance.updater = this._updater;

if (typeof this._instance.componentWillMount === 'function') {
if (
typeof this._instance.UNSAFE_componentWillMount === 'function' ||
typeof this._instance.componentWillMount === 'function'
) {
const beforeState = this._newState;

this._instance.componentWillMount();
if (typeof this._instance.componentWillMount === 'function') {
if (__DEV__) {
if (warnAboutDeprecatedLifecycles) {
const componentName = getName(element.type, this._instance);
if (!didWarnAboutLegacyWillMount[componentName]) {
warning(
false,
'%s: componentWillMount() is deprecated and will be ' +
'removed in the next major version. Read about the motivations ' +
'behind this change: ' +
'https://fb.me/react-async-component-lifecycle-hooks' +
'\n\n' +
'As a temporary workaround, you can rename to ' +
'UNSAFE_componentWillMount instead.',
componentName,
);
didWarnAboutLegacyWillMount[componentName] = true;
}
}
}
this._instance.componentWillMount();
} else {
this._instance.UNSAFE_componentWillMount();
}

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

_updateClassComponent(type, props, context) {
_updateClassComponent(element, context) {
const {props, type} = element;

const oldState = this._instance.state || emptyObject;
const oldProps = this._instance.props;

if (
oldProps !== props &&
typeof this._instance.componentWillReceiveProps === 'function'
) {
this._instance.componentWillReceiveProps(props, context);
if (oldProps !== props) {
if (typeof this._instance.componentWillReceiveProps === 'function') {
if (__DEV__) {
if (warnAboutDeprecatedLifecycles) {
const componentName = getName(element.type, this._instance);
if (!didWarnAboutLegacyWillReceiveProps[componentName]) {
warning(
false,
'%s: componentWillReceiveProps() is deprecated and ' +
'will be removed in the next major version. Use ' +
'static getDerivedStateFromProps() instead. Read about the ' +
'motivations behind this change: ' +
'https://fb.me/react-async-component-lifecycle-hooks' +
'\n\n' +
'As a temporary workaround, you can rename to ' +
'UNSAFE_componentWillReceiveProps instead.',
componentName,
);
didWarnAboutLegacyWillReceiveProps[componentName] = true;
}
}
}
this._instance.componentWillReceiveProps(props, context);
} else if (
typeof this._instance.UNSAFE_componentWillReceiveProps === 'function'
) {
this._instance.UNSAFE_componentWillReceiveProps(props, context);
}

this._updateStateFromStaticLifecycle(props);
}

// Read state after cWRP in case it calls setState
const state = this._newState || oldState;

Expand All @@ -174,7 +272,31 @@ class ReactShallowRenderer {

if (shouldUpdate) {
if (typeof this._instance.componentWillUpdate === 'function') {
if (__DEV__) {
if (warnAboutDeprecatedLifecycles) {
const componentName = getName(element.type, this._instance);
if (!didWarnAboutLegacyWillUpdate[componentName]) {
warning(
false,
'%s: componentWillUpdate() is deprecated and will be ' +
'removed in the next major version. Read about the motivations ' +
'behind this change: ' +
'https://fb.me/react-async-component-lifecycle-hooks' +
'\n\n' +
'As a temporary workaround, you can rename to ' +
'UNSAFE_componentWillUpdate instead.',
componentName,
);
didWarnAboutLegacyWillUpdate[componentName] = true;
}
}
}

this._instance.componentWillUpdate(props, state, context);
} else if (
typeof this._instance.UNSAFE_componentWillUpdate === 'function'
) {
this._instance.UNSAFE_componentWillUpdate(props, state, context);
}
}

Expand All @@ -188,6 +310,58 @@ class ReactShallowRenderer {
// Intentionally do not call componentDidUpdate()
// because DOM refs are not available.
}

_updateStateFromStaticLifecycle(props) {
const {type} = this._element;

if (typeof type.getDerivedStateFromProps === 'function') {
if (__DEV__) {
if (
typeof this._instance.componentWillReceiveProps === 'function' ||
typeof this._instance.UNSAFE_componentWillReceiveProps === 'function'
) {
const componentName = getName(type, this._instance);
if (!didWarnAboutWillReceivePropsAndDerivedState[componentName]) {
warning(
false,
'%s: Defines both componentWillReceiveProps() and static ' +
'getDerivedStateFromProps() methods. We recommend using ' +
'only getDerivedStateFromProps().',
componentName,
);
didWarnAboutWillReceivePropsAndDerivedState[componentName] = true;
}
}
}

const partialState = type.getDerivedStateFromProps.call(
null,
props,
this._instance.state,
);

if (__DEV__) {
if (partialState === undefined) {
const componentName = getName(type, this._instance);
if (!didWarnAboutUndefinedDerivedState[componentName]) {
warning(
false,
'%s.getDerivedStateFromProps(): A valid state object (or null) must be returned. ' +
'You have returned undefined.',
componentName,
);
didWarnAboutUndefinedDerivedState[componentName] = componentName;
}
}
}

if (partialState != null) {
const oldState = this._newState || this._instance.state;
const newState = Object.assign({}, oldState, partialState);
this._instance.state = this._newState = newState;
}
}
}
}

class Updater {
Expand Down
53 changes: 53 additions & 0 deletions src/__tests__/ReactShallowRenderer-test.internal.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/**
* Copyright (c) 2013-present, Facebook, Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @emails react-core
* @jest-environment node
*/

'use strict';

let createRenderer;
let React;
let ReactFeatureFlags;

describe('ReactShallowRenderer', () => {
beforeEach(() => {
ReactFeatureFlags = require('shared/ReactFeatureFlags');
ReactFeatureFlags.warnAboutDeprecatedLifecycles = true;

createRenderer = require('react-test-renderer/shallow').createRenderer;
React = require('react');
});

// TODO (RFC #6) Merge this back into ReactShallowRenderer-test once
// the 'warnAboutDeprecatedLifecycles' feature flag has been removed.
it('should warn if deprecated lifecycles exist', () => {
class ComponentWithWarnings extends React.Component {
componentWillReceiveProps() {}
componentWillMount() {}
componentWillUpdate() {}
render() {
return null;
}
}

const shallowRenderer = createRenderer();
expect(() => shallowRenderer.render(<ComponentWithWarnings />)).toWarnDev(
'Warning: ComponentWithWarnings: componentWillMount() is deprecated and will ' +
'be removed in the next major version.',
);
expect(() => shallowRenderer.render(<ComponentWithWarnings />)).toWarnDev([
'Warning: ComponentWithWarnings: componentWillReceiveProps() is deprecated ' +
'and will be removed in the next major version.',
'Warning: ComponentWithWarnings: componentWillUpdate() is deprecated and will ' +
'be removed in the next major version.',
]);

// Verify no duplicate warnings
shallowRenderer.render(<ComponentWithWarnings />);
});
});
Loading

0 comments on commit 482f6ef

Please sign in to comment.