diff --git a/packages/react-dom/src/__tests__/ReactComponentLifeCycle-test.internal.js b/packages/react-dom/src/__tests__/ReactComponentLifeCycle-test.internal.js index aa7fbf73c1a02..aa9be8e8d4d56 100644 --- a/packages/react-dom/src/__tests__/ReactComponentLifeCycle-test.internal.js +++ b/packages/react-dom/src/__tests__/ReactComponentLifeCycle-test.internal.js @@ -24,6 +24,10 @@ describe('ReactComponentLifeCycle', () => { ReactDOM = require('react-dom'); }); + afterEach(() => { + jest.resetModules(); + }); + // TODO (RFC #6) Merge this back into ReactComponentLifeCycles-test once // the 'warnAboutDeprecatedLifecycles' feature flag has been removed. it('warns about deprecated unsafe lifecycles', function() { @@ -55,4 +59,55 @@ describe('ReactComponentLifeCycle', () => { ReactDOM.render(, container); ReactDOM.render(, container); }); + + describe('react-lifecycles-compat', () => { + // TODO Replace this with react-lifecycles-compat once it's been published + function polyfill(Component) { + Component.prototype.componentWillMount = function() {}; + Component.prototype.componentWillMount.__suppressDeprecationWarning = true; + Component.prototype.componentWillReceiveProps = function() {}; + Component.prototype.componentWillReceiveProps.__suppressDeprecationWarning = true; + } + + it('should not warn about deprecated cWM/cWRP for polyfilled components', () => { + class PolyfilledComponent extends React.Component { + state = {}; + static getDerivedStateFromProps() { + return null; + } + render() { + return null; + } + } + + polyfill(PolyfilledComponent); + + const container = document.createElement('div'); + ReactDOM.render(, container); + }); + + it('should not warn about unsafe lifecycles within "strict" tree for polyfilled components', () => { + const {StrictMode} = React; + + class PolyfilledComponent extends React.Component { + state = {}; + static getDerivedStateFromProps() { + return null; + } + render() { + return null; + } + } + + polyfill(PolyfilledComponent); + + const container = document.createElement('div'); + ReactDOM.render( + + + , + container, + ); + }); + }); }); diff --git a/packages/react-dom/src/__tests__/ReactComponentLifeCycle-test.js b/packages/react-dom/src/__tests__/ReactComponentLifeCycle-test.js index 81591d82fb0ef..b8f53000496ab 100644 --- a/packages/react-dom/src/__tests__/ReactComponentLifeCycle-test.js +++ b/packages/react-dom/src/__tests__/ReactComponentLifeCycle-test.js @@ -499,7 +499,7 @@ describe('ReactComponentLifeCycle', () => { expect(instance.state.stateField).toBe('goodbye'); }); - it('should call nested lifecycle methods in the right order', () => { + it('should call nested legacy lifecycle methods in the right order', () => { let log; const logger = function(msg) { return function() { @@ -509,11 +509,6 @@ describe('ReactComponentLifeCycle', () => { }; }; class Outer extends React.Component { - state = {}; - static getDerivedStateFromProps(props, prevState) { - log.push('outer getDerivedStateFromProps'); - return null; - } UNSAFE_componentWillMount = logger('outer componentWillMount'); componentDidMount = logger('outer componentDidMount'); UNSAFE_componentWillReceiveProps = logger( @@ -533,11 +528,6 @@ describe('ReactComponentLifeCycle', () => { } class Inner extends React.Component { - state = {}; - static getDerivedStateFromProps(props, prevState) { - log.push('inner getDerivedStateFromProps'); - return null; - } UNSAFE_componentWillMount = logger('inner componentWillMount'); componentDidMount = logger('inner componentDidMount'); UNSAFE_componentWillReceiveProps = logger( @@ -554,18 +544,9 @@ describe('ReactComponentLifeCycle', () => { const container = document.createElement('div'); log = []; - expect(() => ReactDOM.render(, container)).toWarnDev([ - 'Warning: Outer: Defines both componentWillReceiveProps() and static ' + - 'getDerivedStateFromProps() methods. ' + - 'We recommend using only getDerivedStateFromProps().', - 'Warning: Inner: Defines both componentWillReceiveProps() and static ' + - 'getDerivedStateFromProps() methods. ' + - 'We recommend using only getDerivedStateFromProps().', - ]); + ReactDOM.render(, container); expect(log).toEqual([ - 'outer getDerivedStateFromProps', 'outer componentWillMount', - 'inner getDerivedStateFromProps', 'inner componentWillMount', 'inner componentDidMount', 'outer componentDidMount', @@ -576,11 +557,9 @@ describe('ReactComponentLifeCycle', () => { ReactDOM.render(, container); expect(log).toEqual([ 'outer componentWillReceiveProps', - 'outer getDerivedStateFromProps', 'outer shouldComponentUpdate', 'outer componentWillUpdate', 'inner componentWillReceiveProps', - 'inner getDerivedStateFromProps', 'inner shouldComponentUpdate', 'inner componentWillUpdate', 'inner componentDidUpdate', @@ -595,6 +574,131 @@ describe('ReactComponentLifeCycle', () => { ]); }); + it('should call nested new lifecycle methods in the right order', () => { + let log; + const logger = function(msg) { + return function() { + // return true for shouldComponentUpdate + log.push(msg); + return true; + }; + }; + class Outer extends React.Component { + state = {}; + static getDerivedStateFromProps(props, prevState) { + log.push('outer getDerivedStateFromProps'); + return null; + } + componentDidMount = logger('outer componentDidMount'); + shouldComponentUpdate = logger('outer shouldComponentUpdate'); + componentDidUpdate = logger('outer componentDidUpdate'); + componentWillUnmount = logger('outer componentWillUnmount'); + render() { + return ( +
+ +
+ ); + } + } + + class Inner extends React.Component { + state = {}; + static getDerivedStateFromProps(props, prevState) { + log.push('inner getDerivedStateFromProps'); + return null; + } + componentDidMount = logger('inner componentDidMount'); + shouldComponentUpdate = logger('inner shouldComponentUpdate'); + componentDidUpdate = logger('inner componentDidUpdate'); + componentWillUnmount = logger('inner componentWillUnmount'); + render() { + return {this.props.x}; + } + } + + const container = document.createElement('div'); + log = []; + ReactDOM.render(, container); + expect(log).toEqual([ + 'outer getDerivedStateFromProps', + 'inner getDerivedStateFromProps', + 'inner componentDidMount', + 'outer componentDidMount', + ]); + + // Dedup warnings + log = []; + ReactDOM.render(, container); + expect(log).toEqual([ + 'outer getDerivedStateFromProps', + 'outer shouldComponentUpdate', + 'inner getDerivedStateFromProps', + 'inner shouldComponentUpdate', + 'inner componentDidUpdate', + 'outer componentDidUpdate', + ]); + + log = []; + ReactDOM.unmountComponentAtNode(container); + expect(log).toEqual([ + 'outer componentWillUnmount', + 'inner componentWillUnmount', + ]); + }); + + it('should not invoke deprecated lifecycles (cWM/cWRP/cWU) if new static gDSFP is present', () => { + class Component extends React.Component { + state = {}; + static getDerivedStateFromProps() { + return null; + } + componentWillMount() { + throw Error('unexpected'); + } + componentWillReceiveProps() { + throw Error('unexpected'); + } + componentWillUpdate() { + throw Error('unexpected'); + } + render() { + return null; + } + } + + const container = document.createElement('div'); + expect(() => ReactDOM.render(, container)).toWarnDev( + 'Defines both componentWillReceiveProps', + ); + }); + + it('should not invoke new unsafe lifecycles (cWM/cWRP/cWU) if static gDSFP is present', () => { + class Component extends React.Component { + state = {}; + static getDerivedStateFromProps() { + return null; + } + UNSAFE_componentWillMount() { + throw Error('unexpected'); + } + UNSAFE_componentWillReceiveProps() { + throw Error('unexpected'); + } + UNSAFE_componentWillUpdate() { + throw Error('unexpected'); + } + render() { + return null; + } + } + + const container = document.createElement('div'); + expect(() => ReactDOM.render(, container)).toWarnDev( + 'Defines both componentWillReceiveProps', + ); + }); + it('calls effects on module-pattern component', function() { const log = []; diff --git a/packages/react-dom/src/__tests__/ReactDOMServerLifecycles-test.internal.js b/packages/react-dom/src/__tests__/ReactDOMServerLifecycles-test.internal.js index 5dd25cc4f718b..5f6e9e10c1ddb 100644 --- a/packages/react-dom/src/__tests__/ReactDOMServerLifecycles-test.internal.js +++ b/packages/react-dom/src/__tests__/ReactDOMServerLifecycles-test.internal.js @@ -22,6 +22,29 @@ describe('ReactDOMServerLifecycles', () => { ReactDOMServer = require('react-dom/server'); }); + afterEach(() => { + jest.resetModules(); + }); + + it('should not invoke cWM if static gDSFP is present', () => { + class Component extends React.Component { + state = {}; + static getDerivedStateFromProps() { + return null; + } + componentWillMount() { + throw Error('unexpected'); + } + render() { + return null; + } + } + + expect(() => ReactDOMServer.renderToString()).toWarnDev( + 'Component: componentWillMount() is deprecated and will be removed in the next major version.', + ); + }); + // TODO (RFC #6) Merge this back into ReactDOMServerLifecycles-test once // the 'warnAboutDeprecatedLifecycles' feature flag has been removed. it('should warn about deprecated lifecycle hooks', () => { @@ -40,4 +63,30 @@ describe('ReactDOMServerLifecycles', () => { // De-duped ReactDOMServer.renderToString(); }); + + describe('react-lifecycles-compat', () => { + // TODO Replace this with react-lifecycles-compat once it's been published + function polyfill(Component) { + Component.prototype.componentWillMount = function() {}; + Component.prototype.componentWillMount.__suppressDeprecationWarning = true; + Component.prototype.componentWillReceiveProps = function() {}; + Component.prototype.componentWillReceiveProps.__suppressDeprecationWarning = true; + } + + it('should not warn about deprecated cWM/cWRP for polyfilled components', () => { + class PolyfilledComponent extends React.Component { + state = {}; + static getDerivedStateFromProps() { + return null; + } + render() { + return null; + } + } + + polyfill(PolyfilledComponent); + + ReactDOMServer.renderToString(); + }); + }); }); diff --git a/packages/react-dom/src/__tests__/ReactDOMServerLifecycles-test.js b/packages/react-dom/src/__tests__/ReactDOMServerLifecycles-test.js index 3de800d0ace3c..10171a0e068e3 100644 --- a/packages/react-dom/src/__tests__/ReactDOMServerLifecycles-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMServerLifecycles-test.js @@ -33,7 +33,7 @@ describe('ReactDOMServerLifecycles', () => { resetModules(); }); - it('should invoke the correct lifecycle hooks', () => { + it('should invoke the correct legacy lifecycle hooks', () => { const log = []; class Outer extends React.Component { @@ -65,6 +65,59 @@ describe('ReactDOMServerLifecycles', () => { ]); }); + it('should invoke the correct new lifecycle hooks', () => { + const log = []; + + class Outer extends React.Component { + state = {}; + static getDerivedStateFromProps() { + log.push('outer getDerivedStateFromProps'); + return null; + } + render() { + log.push('outer render'); + return ; + } + } + + class Inner extends React.Component { + state = {}; + static getDerivedStateFromProps() { + log.push('inner getDerivedStateFromProps'); + return null; + } + render() { + log.push('inner render'); + return null; + } + } + + ReactDOMServer.renderToString(); + expect(log).toEqual([ + 'outer getDerivedStateFromProps', + 'outer render', + 'inner getDerivedStateFromProps', + 'inner render', + ]); + }); + + it('should not invoke unsafe cWM if static gDSFP is present', () => { + class Component extends React.Component { + state = {}; + static getDerivedStateFromProps() { + return null; + } + UNSAFE_componentWillMount() { + throw Error('unexpected'); + } + render() { + return null; + } + } + + ReactDOMServer.renderToString(); + }); + it('should update instance.state with value returned from getDerivedStateFromProps', () => { class Grandparent extends React.Component { state = { diff --git a/packages/react-dom/src/server/ReactPartialRenderer.js b/packages/react-dom/src/server/ReactPartialRenderer.js index 4e6622cac1630..99d409cd12cde 100644 --- a/packages/react-dom/src/server/ReactPartialRenderer.js +++ b/packages/react-dom/src/server/ReactPartialRenderer.js @@ -514,7 +514,10 @@ function resolve( if (inst.UNSAFE_componentWillMount || inst.componentWillMount) { if (inst.componentWillMount) { if (__DEV__) { - if (warnAboutDeprecatedLifecycles) { + if ( + warnAboutDeprecatedLifecycles && + inst.componentWillMount.__suppressDeprecationWarning !== true + ) { const componentName = getComponentName(Component) || 'Unknown'; if (!didWarnAboutDeprecatedWillMount[componentName]) { @@ -534,8 +537,14 @@ function resolve( } } - inst.componentWillMount(); - } else { + // In order to support react-lifecycles-compat polyfilled components, + // Unsafe lifecycles should not be invoked for any component with the new gDSFP. + if (typeof Component.getDerivedStateFromProps !== 'function') { + inst.componentWillMount(); + } + } else if (typeof Component.getDerivedStateFromProps !== 'function') { + // In order to support react-lifecycles-compat polyfilled components, + // Unsafe lifecycles should not be invoked for any component with the new gDSFP. inst.UNSAFE_componentWillMount(); } if (queue.length) { diff --git a/packages/react-reconciler/src/ReactFiberClassComponent.js b/packages/react-reconciler/src/ReactFiberClassComponent.js index 90ca86a992dad..7f83d38d0532a 100644 --- a/packages/react-reconciler/src/ReactFiberClassComponent.js +++ b/packages/react-reconciler/src/ReactFiberClassComponent.js @@ -524,8 +524,11 @@ export default function( if (typeof type.getDerivedStateFromProps === 'function') { if (__DEV__) { + // Don't warn about react-lifecycles-compat polyfilled components if ( - typeof instance.componentWillReceiveProps === 'function' || + (typeof instance.componentWillReceiveProps === 'function' && + instance.componentWillReceiveProps.__suppressDeprecationWarning !== + true) || typeof instance.UNSAFE_componentWillReceiveProps === 'function' ) { const componentName = getComponentName(workInProgress) || 'Unknown'; @@ -627,9 +630,12 @@ export default function( } } + // In order to support react-lifecycles-compat polyfilled components, + // Unsafe lifecycles should not be invoked for any component with the new gDSFP. if ( - typeof instance.UNSAFE_componentWillMount === 'function' || - typeof instance.componentWillMount === 'function' + (typeof instance.UNSAFE_componentWillMount === 'function' || + typeof instance.componentWillMount === 'function') && + typeof workInProgress.type.getDerivedStateFromProps !== 'function' ) { callComponentWillMount(workInProgress, instance); // If we had additional state updates during this life-cycle, let's @@ -775,17 +781,21 @@ export default function( // ever the previously attempted to render - not the "current". However, // during componentDidUpdate we pass the "current" props. + // In order to support react-lifecycles-compat polyfilled components, + // Unsafe lifecycles should not be invoked for any component with the new gDSFP. if ( (typeof instance.UNSAFE_componentWillReceiveProps === 'function' || typeof instance.componentWillReceiveProps === 'function') && - (oldProps !== newProps || oldContext !== newContext) + typeof workInProgress.type.getDerivedStateFromProps !== 'function' ) { - callComponentWillReceiveProps( - workInProgress, - instance, - newProps, - newContext, - ); + if (oldProps !== newProps || oldContext !== newContext) { + callComponentWillReceiveProps( + workInProgress, + instance, + newProps, + newContext, + ); + } } let partialState; @@ -856,9 +866,12 @@ export default function( ); if (shouldUpdate) { + // In order to support react-lifecycles-compat polyfilled components, + // Unsafe lifecycles should not be invoked for any component with the new gDSFP. if ( - typeof instance.UNSAFE_componentWillUpdate === 'function' || - typeof instance.componentWillUpdate === 'function' + (typeof instance.UNSAFE_componentWillUpdate === 'function' || + typeof instance.componentWillUpdate === 'function') && + typeof workInProgress.type.getDerivedStateFromProps !== 'function' ) { if (typeof instance.componentWillUpdate === 'function') { startPhaseTimer(workInProgress, 'componentWillUpdate'); diff --git a/packages/react-reconciler/src/ReactStrictModeWarnings.js b/packages/react-reconciler/src/ReactStrictModeWarnings.js index fb1b15d3d1618..8be9b1dc50936 100644 --- a/packages/react-reconciler/src/ReactStrictModeWarnings.js +++ b/packages/react-reconciler/src/ReactStrictModeWarnings.js @@ -199,6 +199,16 @@ if (__DEV__) { return; } + // Don't warn about react-lifecycles-compat polyfilled components. + // Note that it is sufficient to check for the presence of a + // single lifecycle, componentWillMount, with the polyfill flag. + if ( + typeof instance.componentWillMount === 'function' && + instance.componentWillMount.__suppressDeprecationWarning === true + ) { + return; + } + if (typeof instance.componentWillMount === 'function') { pendingComponentWillMountWarnings.push(fiber); } @@ -225,6 +235,16 @@ if (__DEV__) { return; } + // Don't warn about react-lifecycles-compat polyfilled components. + // Note that it is sufficient to check for the presence of a + // single lifecycle, componentWillMount, with the polyfill flag. + if ( + typeof instance.componentWillMount === 'function' && + instance.componentWillMount.__suppressDeprecationWarning === true + ) { + return; + } + let warningsForRoot; if (!pendingUnsafeLifecycleWarnings.has(strictRoot)) { warningsForRoot = { diff --git a/packages/react-test-renderer/src/ReactShallowRenderer.js b/packages/react-test-renderer/src/ReactShallowRenderer.js index fc2ffbc8551ca..01fd15d28c908 100644 --- a/packages/react-test-renderer/src/ReactShallowRenderer.js +++ b/packages/react-test-renderer/src/ReactShallowRenderer.js @@ -180,7 +180,12 @@ class ReactShallowRenderer { if (typeof this._instance.componentWillMount === 'function') { if (__DEV__) { - if (warnAboutDeprecatedLifecycles) { + // Don't warn about react-lifecycles-compat polyfilled components + if ( + warnAboutDeprecatedLifecycles && + this._instance.componentWillMount.__suppressDeprecationWarning !== + true + ) { const componentName = getName(element.type, this._instance); if (!didWarnAboutLegacyWillMount[componentName]) { warning( @@ -198,8 +203,15 @@ class ReactShallowRenderer { } } } - this._instance.componentWillMount(); - } else { + + // In order to support react-lifecycles-compat polyfilled components, + // Unsafe lifecycles should not be invoked for any component with the new gDSFP. + if (typeof element.type.getDerivedStateFromProps !== 'function') { + this._instance.componentWillMount(); + } + } else if (typeof element.type.getDerivedStateFromProps !== 'function') { + // In order to support react-lifecycles-compat polyfilled components, + // Unsafe lifecycles should not be invoked for any component with the new gDSFP. this._instance.UNSAFE_componentWillMount(); } @@ -242,10 +254,17 @@ class ReactShallowRenderer { } } } - this._instance.componentWillReceiveProps(props, context); + // In order to support react-lifecycles-compat polyfilled components, + // Unsafe lifecycles should not be invoked for any component with the new gDSFP. + if (typeof element.type.getDerivedStateFromProps !== 'function') { + this._instance.componentWillReceiveProps(props, context); + } } else if ( - typeof this._instance.UNSAFE_componentWillReceiveProps === 'function' + typeof this._instance.UNSAFE_componentWillReceiveProps === 'function' && + typeof element.type.getDerivedStateFromProps !== 'function' ) { + // In order to support react-lifecycles-compat polyfilled components, + // Unsafe lifecycles should not be invoked for any component with the new gDSFP. this._instance.UNSAFE_componentWillReceiveProps(props, context); } @@ -292,10 +311,17 @@ class ReactShallowRenderer { } } - this._instance.componentWillUpdate(props, state, context); + // In order to support react-lifecycles-compat polyfilled components, + // Unsafe lifecycles should not be invoked for any component with the new gDSFP. + if (typeof type.getDerivedStateFromProps !== 'function') { + this._instance.componentWillUpdate(props, state, context); + } } else if ( - typeof this._instance.UNSAFE_componentWillUpdate === 'function' + typeof this._instance.UNSAFE_componentWillUpdate === 'function' && + typeof type.getDerivedStateFromProps !== 'function' ) { + // In order to support react-lifecycles-compat polyfilled components, + // Unsafe lifecycles should not be invoked for any component with the new gDSFP. this._instance.UNSAFE_componentWillUpdate(props, state, context); } } @@ -316,8 +342,11 @@ class ReactShallowRenderer { if (typeof type.getDerivedStateFromProps === 'function') { if (__DEV__) { + // Don't warn about react-lifecycles-compat polyfilled components if ( - typeof this._instance.componentWillReceiveProps === 'function' || + (typeof this._instance.componentWillReceiveProps === 'function' && + this._instance.componentWillReceiveProps + .__suppressDeprecationWarning !== true) || typeof this._instance.UNSAFE_componentWillReceiveProps === 'function' ) { const componentName = getName(type, this._instance); diff --git a/packages/react-test-renderer/src/__tests__/ReactShallowRenderer-test.internal.js b/packages/react-test-renderer/src/__tests__/ReactShallowRenderer-test.internal.js index 0c5813f7b2cfe..0d1957a9806e6 100644 --- a/packages/react-test-renderer/src/__tests__/ReactShallowRenderer-test.internal.js +++ b/packages/react-test-renderer/src/__tests__/ReactShallowRenderer-test.internal.js @@ -23,6 +23,10 @@ describe('ReactShallowRenderer', () => { React = require('react'); }); + afterEach(() => { + jest.resetModules(); + }); + // TODO (RFC #6) Merge this back into ReactShallowRenderer-test once // the 'warnAboutDeprecatedLifecycles' feature flag has been removed. it('should warn if deprecated lifecycles exist', () => { @@ -50,4 +54,31 @@ describe('ReactShallowRenderer', () => { // Verify no duplicate warnings shallowRenderer.render(); }); + + describe('react-lifecycles-compat', () => { + // TODO Replace this with react-lifecycles-compat once it's been published + function polyfill(Component) { + Component.prototype.componentWillMount = function() {}; + Component.prototype.componentWillMount.__suppressDeprecationWarning = true; + Component.prototype.componentWillReceiveProps = function() {}; + Component.prototype.componentWillReceiveProps.__suppressDeprecationWarning = true; + } + + it('should not warn about deprecated cWM/cWRP for polyfilled components', () => { + class PolyfilledComponent extends React.Component { + state = {}; + static getDerivedStateFromProps() { + return null; + } + render() { + return null; + } + } + + polyfill(PolyfilledComponent); + + const shallowRenderer = createRenderer(); + shallowRenderer.render(); + }); + }); }); diff --git a/packages/react-test-renderer/src/__tests__/ReactShallowRenderer-test.js b/packages/react-test-renderer/src/__tests__/ReactShallowRenderer-test.js index 02532ec6fb799..240129421efa6 100644 --- a/packages/react-test-renderer/src/__tests__/ReactShallowRenderer-test.js +++ b/packages/react-test-renderer/src/__tests__/ReactShallowRenderer-test.js @@ -23,13 +23,11 @@ describe('ReactShallowRenderer', () => { React = require('react'); }); - it('should call all of the lifecycle hooks', () => { + it('should call all of the legacy lifecycle hooks', () => { const logs = []; const logger = message => () => logs.push(message) || true; class SomeComponent extends React.Component { - state = {}; - static getDerivedStateFromProps = logger('getDerivedStateFromProps'); UNSAFE_componentWillMount = logger('componentWillMount'); componentDidMount = logger('componentDidMount'); UNSAFE_componentWillReceiveProps = logger('componentWillReceiveProps'); @@ -43,16 +41,11 @@ describe('ReactShallowRenderer', () => { } const shallowRenderer = createRenderer(); - - expect(() => shallowRenderer.render()).toWarnDev( - 'Warning: SomeComponent: Defines both componentWillReceiveProps() and static ' + - 'getDerivedStateFromProps() methods. ' + - 'We recommend using only getDerivedStateFromProps().', - ); + shallowRenderer.render(); // Calling cDU might lead to problems with host component references. // Since our components aren't really mounted, refs won't be available. - expect(logs).toEqual(['getDerivedStateFromProps', 'componentWillMount']); + expect(logs).toEqual(['componentWillMount']); logs.splice(0); @@ -68,12 +61,75 @@ describe('ReactShallowRenderer', () => { // The previous shallow renderer did not trigger cDU for props changes. expect(logs).toEqual([ 'componentWillReceiveProps', - 'getDerivedStateFromProps', 'shouldComponentUpdate', 'componentWillUpdate', ]); }); + it('should call all of the new lifecycle hooks', () => { + const logs = []; + const logger = message => () => logs.push(message) || true; + + class SomeComponent extends React.Component { + state = {}; + static getDerivedStateFromProps = logger('getDerivedStateFromProps'); + componentDidMount = logger('componentDidMount'); + shouldComponentUpdate = logger('shouldComponentUpdate'); + componentDidUpdate = logger('componentDidUpdate'); + componentWillUnmount = logger('componentWillUnmount'); + render() { + return
; + } + } + + const shallowRenderer = createRenderer(); + shallowRenderer.render(); + + // Calling cDU might lead to problems with host component references. + // Since our components aren't really mounted, refs won't be available. + expect(logs).toEqual(['getDerivedStateFromProps']); + + logs.splice(0); + + const instance = shallowRenderer.getMountedInstance(); + instance.setState({}); + + expect(logs).toEqual(['shouldComponentUpdate']); + + logs.splice(0); + + shallowRenderer.render(); + + // The previous shallow renderer did not trigger cDU for props changes. + expect(logs).toEqual(['getDerivedStateFromProps', 'shouldComponentUpdate']); + }); + + it('should not invoke deprecated lifecycles (cWM/cWRP/cWU) if new static gDSFP is present', () => { + class Component extends React.Component { + state = {}; + static getDerivedStateFromProps() { + return null; + } + componentWillMount() { + throw Error('unexpected'); + } + componentWillReceiveProps() { + throw Error('unexpected'); + } + componentWillUpdate() { + throw Error('unexpected'); + } + render() { + return null; + } + } + + const shallowRenderer = createRenderer(); + expect(() => shallowRenderer.render()).toWarnDev( + 'Defines both componentWillReceiveProps() and static getDerivedStateFromProps()', + ); + }); + it('should only render 1 level deep', () => { function Parent() { return ( @@ -422,11 +478,10 @@ describe('ReactShallowRenderer', () => { expect(result).toEqual(
); }); - it('passes expected params to component lifecycle methods', () => { + it('passes expected params to legacy component lifecycle methods', () => { const componentDidUpdateParams = []; const componentWillReceivePropsParams = []; const componentWillUpdateParams = []; - const getDerivedStateFromPropsParams = []; const setStateParams = []; const shouldComponentUpdateParams = []; @@ -448,10 +503,6 @@ describe('ReactShallowRenderer', () => { componentDidUpdate(...args) { componentDidUpdateParams.push(...args); } - static getDerivedStateFromProps(...args) { - getDerivedStateFromPropsParams.push(args); - return null; - } UNSAFE_componentWillReceiveProps(...args) { componentWillReceivePropsParams.push(...args); this.setState((...innerArgs) => { @@ -472,22 +523,10 @@ describe('ReactShallowRenderer', () => { } const shallowRenderer = createRenderer(); - - // The only lifecycle hook that should be invoked on initial render - // Is the static getDerivedStateFromProps() methods - expect(() => - shallowRenderer.render( - React.createElement(SimpleComponent, initialProp), - initialContext, - ), - ).toWarnDev( - 'SimpleComponent: Defines both componentWillReceiveProps() and static ' + - 'getDerivedStateFromProps() methods. We recommend using ' + - 'only getDerivedStateFromProps().', + shallowRenderer.render( + React.createElement(SimpleComponent, initialProp), + initialContext, ); - expect(getDerivedStateFromPropsParams).toEqual([ - [initialProp, initialState], - ]); expect(componentDidUpdateParams).toEqual([]); expect(componentWillReceivePropsParams).toEqual([]); expect(componentWillUpdateParams).toEqual([]); @@ -504,10 +543,6 @@ describe('ReactShallowRenderer', () => { updatedContext, ]); expect(setStateParams).toEqual([initialState, initialProp]); - expect(getDerivedStateFromPropsParams).toEqual([ - [initialProp, initialState], - [updatedProp, initialState], - ]); expect(shouldComponentUpdateParams).toEqual([ updatedProp, updatedState, @@ -521,6 +556,72 @@ describe('ReactShallowRenderer', () => { expect(componentDidUpdateParams).toEqual([]); }); + it('passes expected params to new component lifecycle methods', () => { + const componentDidUpdateParams = []; + const getDerivedStateFromPropsParams = []; + const shouldComponentUpdateParams = []; + + const initialProp = {prop: 'init prop'}; + const initialState = {state: 'init state'}; + const initialContext = {context: 'init context'}; + const updatedProp = {prop: 'updated prop'}; + const updatedContext = {context: 'updated context'}; + + class SimpleComponent extends React.Component { + constructor(props, context) { + super(props, context); + this.state = initialState; + } + static contextTypes = { + context: PropTypes.string, + }; + componentDidUpdate(...args) { + componentDidUpdateParams.push(...args); + } + static getDerivedStateFromProps(...args) { + getDerivedStateFromPropsParams.push(args); + return null; + } + shouldComponentUpdate(...args) { + shouldComponentUpdateParams.push(...args); + return true; + } + render() { + return null; + } + } + + const shallowRenderer = createRenderer(); + + // The only lifecycle hook that should be invoked on initial render + // Is the static getDerivedStateFromProps() methods + shallowRenderer.render( + React.createElement(SimpleComponent, initialProp), + initialContext, + ); + expect(getDerivedStateFromPropsParams).toEqual([ + [initialProp, initialState], + ]); + expect(componentDidUpdateParams).toEqual([]); + expect(shouldComponentUpdateParams).toEqual([]); + + // Lifecycle hooks should be invoked with the correct prev/next params on update. + shallowRenderer.render( + React.createElement(SimpleComponent, updatedProp), + updatedContext, + ); + expect(getDerivedStateFromPropsParams).toEqual([ + [initialProp, initialState], + [updatedProp, initialState], + ]); + expect(shouldComponentUpdateParams).toEqual([ + updatedProp, + initialState, + updatedContext, + ]); + expect(componentDidUpdateParams).toEqual([]); + }); + it('can shallowly render components with ref as function', () => { class SimpleComponent extends React.Component { state = {clicked: false}; diff --git a/packages/react/src/__tests__/ReactStrictMode-test.internal.js b/packages/react/src/__tests__/ReactStrictMode-test.internal.js index 5bab8df2dd092..edbc5175a0b35 100644 --- a/packages/react/src/__tests__/ReactStrictMode-test.internal.js +++ b/packages/react/src/__tests__/ReactStrictMode-test.internal.js @@ -42,18 +42,9 @@ describe('ReactStrictMode', () => { componentDidUpdate() { log.push('componentDidUpdate'); } - UNSAFE_componentWillMount() { - log.push('componentWillMount'); - } - UNSAFE_componentWillReceiveProps() { - log.push('componentWillReceiveProps'); - } componentWillUnmount() { log.push('componentWillUnmount'); } - UNSAFE_componentWillUpdate() { - log.push('componentWillUpdate'); - } shouldComponentUpdate() { log.push('shouldComponentUpdate'); return shouldComponentUpdate; @@ -64,22 +55,13 @@ describe('ReactStrictMode', () => { } } - let component; - - expect(() => { - component = ReactTestRenderer.create(); - }).toWarnDev( - 'ClassComponent: Defines both componentWillReceiveProps() ' + - 'and static getDerivedStateFromProps() methods. ' + - 'We recommend using only getDerivedStateFromProps().', - ); + const component = ReactTestRenderer.create(); expect(log).toEqual([ 'constructor', 'constructor', 'getDerivedStateFromProps', 'getDerivedStateFromProps', - 'componentWillMount', 'render', 'render', 'componentDidMount', @@ -90,11 +72,9 @@ describe('ReactStrictMode', () => { component.update(); expect(log).toEqual([ - 'componentWillReceiveProps', 'getDerivedStateFromProps', 'getDerivedStateFromProps', 'shouldComponentUpdate', - 'componentWillUpdate', 'render', 'render', 'componentDidUpdate', @@ -105,7 +85,6 @@ describe('ReactStrictMode', () => { component.update(); expect(log).toEqual([ - 'componentWillReceiveProps', 'getDerivedStateFromProps', 'getDerivedStateFromProps', 'shouldComponentUpdate', diff --git a/packages/react/src/__tests__/createReactClassIntegration-test.js b/packages/react/src/__tests__/createReactClassIntegration-test.js index aa46acf4fc476..c6156e7ea80e1 100644 --- a/packages/react/src/__tests__/createReactClassIntegration-test.js +++ b/packages/react/src/__tests__/createReactClassIntegration-test.js @@ -449,4 +449,34 @@ describe('create-react-class-integration', () => { ReactDOM.render(, document.createElement('div')), ).toWarnDev('Did not properly initialize state during construction.'); }); + + it('should not invoke deprecated lifecycles (cWM/cWRP/cWU) if new static gDSFP is present', () => { + const Component = createReactClass({ + statics: { + getDerivedStateFromProps: function() { + return null; + }, + }, + componentWillMount: function() { + throw Error('unexpected'); + }, + componentWillReceiveProps: function() { + throw Error('unexpected'); + }, + componentWillUpdate: function() { + throw Error('unexpected'); + }, + getInitialState: function() { + return {}; + }, + render: function() { + return null; + }, + }); + + expect(() => { + ReactDOM.render(, document.createElement('div')); + }).toWarnDev('Defines both componentWillReceiveProps'); + ReactDOM.render(, document.createElement('div')); + }); });