Repository: facebook/react
Pull request: Expanded DEV-only warnings for gDSFP and legacy lifecycles

After a lot of discussion and careful consideration, the team has reaffirmed the initial decision to not call _unsafe_ legacy lifecycles `componentWillMount`, `componentWillReceiveProps`, and `componentWillUpdate` for any component that defines the new static `getDerivedStateFromProps` lifecycle. (This applies to the "UNSAFE_" aliases as well.)

The reason for this decision ultimately boils down to support for the [react-lifecycles-compat polyfill](https://github.com/reactjs/react-lifecycles-compat) and concerns about consistent, predictable behavior for future version of React.

As @jquense pointed out with issue #12411, conditional lifecycle behavior is a _new_ thing, and it is not intuitive. For this reason, we needed to improve the granularity and wording of the DEV warning. This PR does that, first by expanding the warning conditionals to explicitly check for `componentWillMount` and `componentWillUpdate` as well (rather than just `componentWillReceiveProps`) and secondly by explicitly stating that "_Unsafe legacy lifecycles will not be called for components using the new getDerivedStateFromProps() API_".

The entire new DEV warning is:
```
Unsafe legacy lifecycles will not be called for components using the new 
getDerivedStateFromProps() API.

<YourComponentName> uses getDerivedStateFromProps() but also contains 
the following legacy lifecycles:
  componentWillMount
  componentWillReceiveProps
  componentWillUpdate

The above lifecycles should be removed. Learn more about this warning here:
https://fb.me/react-async-component-lifecycle-hooks
```

Tests have been updated as well to verify the new behavior.

Resolves #12411

Changed files:
filename: packages/react-dom/src/__tests__/ReactComponentLifeCycle-test.js
patch: @@ -669,7 +669,7 @@ describe('ReactComponentLifeCycle', () => {
 
     const container = document.createElement('div');
     expect(() => ReactDOM.render(<Component />, container)).toWarnDev(
-      'Defines both componentWillReceiveProps',
+      'Unsafe legacy lifecycles will not be called for components using the new getDerivedStateFromProps() API.',
     );
   });
 
@@ -695,7 +695,93 @@ describe('ReactComponentLifeCycle', () => {
 
     const container = document.createElement('div');
     expect(() => ReactDOM.render(<Component />, container)).toWarnDev(
-      'Defines both componentWillReceiveProps',
+      'Unsafe legacy lifecycles will not be called for components using the new getDerivedStateFromProps() API.',
+    );
+  });
+
+  it('should warn about deprecated lifecycles (cWM/cWRP/cWU) if new static gDSFP is present', () => {
+    const container = document.createElement('div');
+
+    class AllLegacyLifecycles extends React.Component {
+      state = {};
+      static getDerivedStateFromProps() {
+        return null;
+      }
+      componentWillMount() {}
+      UNSAFE_componentWillReceiveProps() {}
+      componentWillUpdate() {}
+      render() {
+        return null;
+      }
+    }
+
+    expect(() => ReactDOM.render(<AllLegacyLifecycles />, container)).toWarnDev(
+      'Unsafe legacy lifecycles will not be called for components using the new getDerivedStateFromProps() API.\n\n' +
+        'AllLegacyLifecycles uses getDerivedStateFromProps() but also contains the following legacy lifecycles:\n' +
+        '  componentWillMount\n' +
+        '  UNSAFE_componentWillReceiveProps\n' +
+        '  componentWillUpdate\n\n' +
+        'The above lifecycles should be removed. Learn more about this warning here:\n' +
+        'https://fb.me/react-async-component-lifecycle-hooks',
+    );
+
+    class WillMount extends React.Component {
+      state = {};
+      static getDerivedStateFromProps() {
+        return null;
+      }
+      UNSAFE_componentWillMount() {}
+      render() {
+        return null;
+      }
+    }
+
+    expect(() => ReactDOM.render(<WillMount />, container)).toWarnDev(
+      'Unsafe legacy lifecycles will not be called for components using the new getDerivedStateFromProps() API.\n\n' +
+        'WillMount uses getDerivedStateFromProps() but also contains the following legacy lifecycles:\n' +
+        '  UNSAFE_componentWillMount\n\n' +
+        'The above lifecycles should be removed. Learn more about this warning here:\n' +
+        'https://fb.me/react-async-component-lifecycle-hooks',
+    );
+
+    class WillMountAndUpdate extends React.Component {
+      state = {};
+      static getDerivedStateFromProps() {
+        return null;
+      }
+      componentWillMount() {}
+      UNSAFE_componentWillUpdate() {}
+      render() {
+        return null;
+      }
+    }
+
+    expect(() => ReactDOM.render(<WillMountAndUpdate />, container)).toWarnDev(
+      'Unsafe legacy lifecycles will not be called for components using the new getDerivedStateFromProps() API.\n\n' +
+        'WillMountAndUpdate uses getDerivedStateFromProps() but also contains the following legacy lifecycles:\n' +
+        '  componentWillMount\n' +
+        '  UNSAFE_componentWillUpdate\n\n' +
+        'The above lifecycles should be removed. Learn more about this warning here:\n' +
+        'https://fb.me/react-async-component-lifecycle-hooks',
+    );
+
+    class WillReceiveProps extends React.Component {
+      state = {};
+      static getDerivedStateFromProps() {
+        return null;
+      }
+      componentWillReceiveProps() {}
+      render() {
+        return null;
+      }
+    }
+
+    expect(() => ReactDOM.render(<WillReceiveProps />, container)).toWarnDev(
+      'Unsafe legacy lifecycles will not be called for components using the new getDerivedStateFromProps() API.\n\n' +
+        'WillReceiveProps uses getDerivedStateFromProps() but also contains the following legacy lifecycles:\n' +
+        '  componentWillReceiveProps\n\n' +
+        'The above lifecycles should be removed. Learn more about this warning here:\n' +
+        'https://fb.me/react-async-component-lifecycle-hooks',
     );
   });
 

filename: packages/react-reconciler/src/ReactFiberClassComponent.js
patch: @@ -41,14 +41,14 @@ const isArray = Array.isArray;
 let didWarnAboutStateAssignmentForComponent;
 let didWarnAboutUndefinedDerivedState;
 let didWarnAboutUninitializedState;
-let didWarnAboutWillReceivePropsAndDerivedState;
+let didWarnAboutLegacyLifecyclesAndDerivedState;
 let warnOnInvalidCallback;
 
 if (__DEV__) {
   didWarnAboutStateAssignmentForComponent = {};
   didWarnAboutUndefinedDerivedState = {};
   didWarnAboutUninitializedState = {};
-  didWarnAboutWillReceivePropsAndDerivedState = {};
+  didWarnAboutLegacyLifecyclesAndDerivedState = {};
 
   const didWarnOnInvalidCallback = {};
 
@@ -435,20 +435,75 @@ export default function(
     adoptClassInstance(workInProgress, instance);
 
     if (__DEV__) {
-      if (
-        typeof ctor.getDerivedStateFromProps === 'function' &&
-        state === null
-      ) {
-        const componentName = getComponentName(workInProgress) || 'Component';
-        if (!didWarnAboutUninitializedState[componentName]) {
-          warning(
-            false,
-            '%s: Did not properly initialize state during construction. ' +
-              'Expected state to be an object, but it was %s.',
-            componentName,
-            instance.state === null ? 'null' : 'undefined',
-          );
-          didWarnAboutUninitializedState[componentName] = true;
+      if (typeof ctor.getDerivedStateFromProps === 'function') {
+        if (state === null) {
+          const componentName = getComponentName(workInProgress) || 'Component';
+          if (!didWarnAboutUninitializedState[componentName]) {
+            warning(
+              false,
+              '%s: Did not properly initialize state during construction. ' +
+                'Expected state to be an object, but it was %s.',
+              componentName,
+              instance.state === null ? 'null' : 'undefined',
+            );
+            didWarnAboutUninitializedState[componentName] = true;
+          }
+        }
+
+        // If getDerivedStateFromProps() is defined, "unsafe" lifecycles won't be called.
+        // Warn about these lifecycles if they are present.
+        // Don't warn about react-lifecycles-compat polyfilled methods though.
+        let foundWillMountName = null;
+        let foundWillReceivePropsName = null;
+        let foundWillUpdateName = null;
+        if (
+          typeof instance.componentWillMount === 'function' &&
+          instance.componentWillMount.__suppressDeprecationWarning !== true
+        ) {
+          foundWillMountName = 'componentWillMount';
+        } else if (typeof instance.UNSAFE_componentWillMount === 'function') {
+          foundWillMountName = 'UNSAFE_componentWillMount';
+        }
+        if (
+          typeof instance.componentWillReceiveProps === 'function' &&
+          instance.componentWillReceiveProps.__suppressDeprecationWarning !==
+            true
+        ) {
+          foundWillReceivePropsName = 'componentWillReceiveProps';
+        } else if (
+          typeof instance.UNSAFE_componentWillReceiveProps === 'function'
+        ) {
+          foundWillReceivePropsName = 'UNSAFE_componentWillReceiveProps';
+        }
+        if (typeof instance.componentWillUpdate === 'function') {
+          foundWillUpdateName = 'componentWillUpdate';
+        } else if (typeof instance.UNSAFE_componentWillUpdate === 'function') {
+          foundWillUpdateName = 'UNSAFE_componentWillUpdate';
+        }
+        if (
+          foundWillMountName !== null ||
+          foundWillReceivePropsName !== null ||
+          foundWillUpdateName !== null
+        ) {
+          const componentName = getComponentName(workInProgress) || 'Component';
+          if (!didWarnAboutLegacyLifecyclesAndDerivedState[componentName]) {
+            warning(
+              false,
+              'Unsafe legacy lifecycles will not be called for components using ' +
+                'the new getDerivedStateFromProps() API.\n\n' +
+                '%s uses getDerivedStateFromProps() but also contains the following legacy lifecycles:' +
+                '%s%s%s\n\n' +
+                'The above lifecycles should be removed. Learn more about this warning here:\n' +
+                'https://fb.me/react-async-component-lifecycle-hooks',
+              componentName,
+              foundWillMountName !== null ? `\n  ${foundWillMountName}` : '',
+              foundWillReceivePropsName !== null
+                ? `\n  ${foundWillReceivePropsName}`
+                : '',
+              foundWillUpdateName !== null ? `\n  ${foundWillUpdateName}` : '',
+            );
+            didWarnAboutLegacyLifecyclesAndDerivedState[componentName] = true;
+          }
         }
       }
     }
@@ -552,28 +607,6 @@ export default function(
     const {type} = workInProgress;
 
     if (typeof type.getDerivedStateFromProps === 'function') {
-      if (__DEV__) {
-        // Don't warn about react-lifecycles-compat polyfilled components
-        if (
-          (typeof instance.componentWillReceiveProps === 'function' &&
-            instance.componentWillReceiveProps.__suppressDeprecationWarning !==
-              true) ||
-          typeof instance.UNSAFE_componentWillReceiveProps === 'function'
-        ) {
-          const componentName = getComponentName(workInProgress) || 'Component';
-          if (!didWarnAboutWillReceivePropsAndDerivedState[componentName]) {
-            warning(
-              false,
-              '%s: Defines both componentWillReceiveProps() and static ' +
-                'getDerivedStateFromProps() methods. We recommend using ' +
-                'only getDerivedStateFromProps().',
-              componentName,
-            );
-            didWarnAboutWillReceivePropsAndDerivedState[componentName] = true;
-          }
-        }
-      }
-
       if (
         debugRenderPhaseSideEffects ||
         (debugRenderPhaseSideEffectsForStrictMode &&

filename: packages/react-test-renderer/src/ReactShallowRenderer.js
patch: @@ -23,7 +23,7 @@ let didWarnAboutLegacyWillReceiveProps;
 let didWarnAboutLegacyWillUpdate;
 let didWarnAboutUndefinedDerivedState;
 let didWarnAboutUninitializedState;
-let didWarnAboutWillReceivePropsAndDerivedState;
+let didWarnAboutLegacyLifecyclesAndDerivedState;
 
 if (__DEV__) {
   if (warnAboutDeprecatedLifecycles) {
@@ -33,7 +33,7 @@ if (__DEV__) {
   }
   didWarnAboutUndefinedDerivedState = {};
   didWarnAboutUninitializedState = {};
-  didWarnAboutWillReceivePropsAndDerivedState = {};
+  didWarnAboutLegacyLifecyclesAndDerivedState = {};
 }
 
 class ReactShallowRenderer {
@@ -352,23 +352,61 @@ class ReactShallowRenderer {
 
     if (typeof type.getDerivedStateFromProps === 'function') {
       if (__DEV__) {
-        // Don't warn about react-lifecycles-compat polyfilled components
+        const instance = this._instance;
+
+        // If getDerivedStateFromProps() is defined, "unsafe" lifecycles won't be called.
+        // Warn about these lifecycles if they are present.
+        // Don't warn about react-lifecycles-compat polyfilled methods though.
+        let foundWillMountName = null;
+        let foundWillReceivePropsName = null;
+        let foundWillUpdateName = null;
         if (
-          (typeof this._instance.componentWillReceiveProps === 'function' &&
-            this._instance.componentWillReceiveProps
-              .__suppressDeprecationWarning !== true) ||
-          typeof this._instance.UNSAFE_componentWillReceiveProps === 'function'
+          typeof instance.componentWillMount === 'function' &&
+          instance.componentWillMount.__suppressDeprecationWarning !== true
         ) {
-          const componentName = getName(type, this._instance);
-          if (!didWarnAboutWillReceivePropsAndDerivedState[componentName]) {
+          foundWillMountName = 'componentWillMount';
+        } else if (typeof instance.UNSAFE_componentWillMount === 'function') {
+          foundWillMountName = 'UNSAFE_componentWillMount';
+        }
+        if (
+          typeof instance.componentWillReceiveProps === 'function' &&
+          instance.componentWillReceiveProps.__suppressDeprecationWarning !==
+            true
+        ) {
+          foundWillReceivePropsName = 'componentWillReceiveProps';
+        } else if (
+          typeof instance.UNSAFE_componentWillReceiveProps === 'function'
+        ) {
+          foundWillReceivePropsName = 'UNSAFE_componentWillReceiveProps';
+        }
+        if (typeof instance.componentWillUpdate === 'function') {
+          foundWillUpdateName = 'componentWillUpdate';
+        } else if (typeof instance.UNSAFE_componentWillUpdate === 'function') {
+          foundWillUpdateName = 'UNSAFE_componentWillUpdate';
+        }
+        if (
+          foundWillMountName !== null ||
+          foundWillReceivePropsName !== null ||
+          foundWillUpdateName !== null
+        ) {
+          const componentName = getName(type, instance) || 'Component';
+          if (!didWarnAboutLegacyLifecyclesAndDerivedState[componentName]) {
             warning(
               false,
-              '%s: Defines both componentWillReceiveProps() and static ' +
-                'getDerivedStateFromProps() methods. We recommend using ' +
-                'only getDerivedStateFromProps().',
+              'Unsafe legacy lifecycles will not be called for components using ' +
+                'the new getDerivedStateFromProps() API.\n\n' +
+                '%s uses getDerivedStateFromProps() but also contains the following legacy lifecycles:' +
+                '%s%s%s\n\n' +
+                'The above lifecycles should be removed. Learn more about this warning here:\n' +
+                'https://fb.me/react-async-component-lifecycle-hooks',
               componentName,
+              foundWillMountName !== null ? `\n  ${foundWillMountName}` : '',
+              foundWillReceivePropsName !== null
+                ? `\n  ${foundWillReceivePropsName}`
+                : '',
+              foundWillUpdateName !== null ? `\n  ${foundWillUpdateName}` : '',
             );
-            didWarnAboutWillReceivePropsAndDerivedState[componentName] = true;
+            didWarnAboutLegacyLifecyclesAndDerivedState[componentName] = true;
           }
         }
       }

filename: packages/react-test-renderer/src/__tests__/ReactShallowRenderer-test.js
patch: @@ -125,8 +125,98 @@ describe('ReactShallowRenderer', () => {
     }
 
     const shallowRenderer = createRenderer();
-    expect(() => shallowRenderer.render(<Component foo={2} />)).toWarnDev(
-      'Defines both componentWillReceiveProps() and static getDerivedStateFromProps()',
+    expect(() => shallowRenderer.render(<Component />)).toWarnDev(
+      'Unsafe legacy lifecycles will not be called for components using the new getDerivedStateFromProps() API.',
+    );
+  });
+
+  it('should warn about deprecated lifecycles (cWM/cWRP/cWU) if new static gDSFP is present', () => {
+    let shallowRenderer;
+
+    class AllLegacyLifecycles extends React.Component {
+      state = {};
+      static getDerivedStateFromProps() {
+        return null;
+      }
+      componentWillMount() {}
+      UNSAFE_componentWillReceiveProps() {}
+      componentWillUpdate() {}
+      render() {
+        return null;
+      }
+    }
+
+    shallowRenderer = createRenderer();
+    expect(() => shallowRenderer.render(<AllLegacyLifecycles />)).toWarnDev(
+      'Unsafe legacy lifecycles will not be called for components using the new getDerivedStateFromProps() API.\n\n' +
+        'AllLegacyLifecycles uses getDerivedStateFromProps() but also contains the following legacy lifecycles:\n' +
+        '  componentWillMount\n' +
+        '  UNSAFE_componentWillReceiveProps\n' +
+        '  componentWillUpdate\n\n' +
+        'The above lifecycles should be removed. Learn more about this warning here:\n' +
+        'https://fb.me/react-async-component-lifecycle-hooks',
+    );
+
+    class WillMount extends React.Component {
+      state = {};
+      static getDerivedStateFromProps() {
+        return null;
+      }
+      UNSAFE_componentWillMount() {}
+      render() {
+        return null;
+      }
+    }
+
+    shallowRenderer = createRenderer();
+    expect(() => shallowRenderer.render(<WillMount />)).toWarnDev(
+      'Unsafe legacy lifecycles will not be called for components using the new getDerivedStateFromProps() API.\n\n' +
+        'WillMount uses getDerivedStateFromProps() but also contains the following legacy lifecycles:\n' +
+        '  UNSAFE_componentWillMount\n\n' +
+        'The above lifecycles should be removed. Learn more about this warning here:\n' +
+        'https://fb.me/react-async-component-lifecycle-hooks',
+    );
+
+    class WillMountAndUpdate extends React.Component {
+      state = {};
+      static getDerivedStateFromProps() {
+        return null;
+      }
+      componentWillMount() {}
+      UNSAFE_componentWillUpdate() {}
+      render() {
+        return null;
+      }
+    }
+
+    shallowRenderer = createRenderer();
+    expect(() => shallowRenderer.render(<WillMountAndUpdate />)).toWarnDev(
+      'Unsafe legacy lifecycles will not be called for components using the new getDerivedStateFromProps() API.\n\n' +
+        'WillMountAndUpdate uses getDerivedStateFromProps() but also contains the following legacy lifecycles:\n' +
+        '  componentWillMount\n' +
+        '  UNSAFE_componentWillUpdate\n\n' +
+        'The above lifecycles should be removed. Learn more about this warning here:\n' +
+        'https://fb.me/react-async-component-lifecycle-hooks',
+    );
+
+    class WillReceiveProps extends React.Component {
+      state = {};
+      static getDerivedStateFromProps() {
+        return null;
+      }
+      componentWillReceiveProps() {}
+      render() {
+        return null;
+      }
+    }
+
+    shallowRenderer = createRenderer();
+    expect(() => shallowRenderer.render(<WillReceiveProps />)).toWarnDev(
+      'Unsafe legacy lifecycles will not be called for components using the new getDerivedStateFromProps() API.\n\n' +
+        'WillReceiveProps uses getDerivedStateFromProps() but also contains the following legacy lifecycles:\n' +
+        '  componentWillReceiveProps\n\n' +
+        'The above lifecycles should be removed. Learn more about this warning here:\n' +
+        'https://fb.me/react-async-component-lifecycle-hooks',
     );
   });
 
@@ -1238,9 +1328,7 @@ describe('ReactShallowRenderer', () => {
 
     const shallowRenderer = createRenderer();
     expect(() => shallowRenderer.render(<ComponentWithWarnings />)).toWarnDev(
-      'ComponentWithWarnings: Defines both componentWillReceiveProps() and static ' +
-        'getDerivedStateFromProps() methods. We recommend using ' +
-        'only getDerivedStateFromProps().',
+      'ComponentWithWarnings uses getDerivedStateFromProps() but also contains the following legacy lifecycles',
     );
 
     // Should not log duplicate warning

filename: packages/react/src/__tests__/createReactClassIntegration-test.js
patch: @@ -509,7 +509,15 @@ describe('create-react-class-integration', () => {
 
     expect(() => {
       ReactDOM.render(<Component />, document.createElement('div'));
-    }).toWarnDev('Defines both componentWillReceiveProps');
+    }).toWarnDev(
+      'Unsafe legacy lifecycles will not be called for components using the new getDerivedStateFromProps() API.\n\n' +
+        'Component uses getDerivedStateFromProps() but also contains the following legacy lifecycles:\n' +
+        '  componentWillMount\n' +
+        '  componentWillReceiveProps\n' +
+        '  componentWillUpdate\n\n' +
+        'The above lifecycles should be removed. Learn more about this warning here:\n' +
+        'https://fb.me/react-async-component-lifecycle-hooks',
+    );
     ReactDOM.render(<Component foo={1} />, document.createElement('div'));
   });