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
This commit is contained in:
Brian Vaughn
2018-01-19 09:36:46 -08:00
committed by GitHub
parent fccd11bec0
commit 97e2911508
38 changed files with 1837 additions and 265 deletions
@@ -275,7 +275,7 @@ describe('ReactCallReturn', () => {
return ReactCallReturn.unstable_createReturn(this.props.children);
}
componentWillMount() {
UNSAFE_componentWillMount() {
ops.push(`Mount Return ${this.props.value}`);
}
@@ -0,0 +1,56 @@
/**
* 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
*/
'use strict';
let React;
let ReactDOM;
let ReactFeatureFlags;
describe('ReactComponentLifeCycle', () => {
beforeEach(() => {
jest.resetModules();
ReactFeatureFlags = require('shared/ReactFeatureFlags');
ReactFeatureFlags.warnAboutDeprecatedLifecycles = true;
React = require('react');
ReactDOM = require('react-dom');
});
// TODO (RFC #6) Merge this back into ReactComponentLifeCycles-test once
// the 'warnAboutDeprecatedLifecycles' feature flag has been removed.
it('warns about deprecated unsafe lifecycles', function() {
class MyComponent extends React.Component {
componentWillMount() {}
componentWillReceiveProps() {}
componentWillUpdate() {}
render() {
return null;
}
}
const container = document.createElement('div');
expect(() => ReactDOM.render(<MyComponent x={1} />, container)).toWarnDev([
'Warning: MyComponent: componentWillMount() is deprecated and will be ' +
'removed in the next major version.',
]);
expect(() => ReactDOM.render(<MyComponent x={2} />, container)).toWarnDev([
'Warning: MyComponent: componentWillReceiveProps() is deprecated and ' +
'will be removed in the next major version.',
'Warning: MyComponent: componentWillUpdate() is deprecated and will be ' +
'removed in the next major version.',
]);
// Dedupe check (instantiate and update)
ReactDOM.render(<MyComponent key="new" x={1} />, container);
ReactDOM.render(<MyComponent key="new" x={2} />, container);
});
});
@@ -165,7 +165,7 @@ describe('ReactComponentLifeCycle', () => {
// had provided a getInitialState method.
it('throws when accessing state in componentWillMount', () => {
class StatefulComponent extends React.Component {
componentWillMount() {
UNSAFE_componentWillMount() {
void this.state.yada;
}
@@ -182,7 +182,7 @@ describe('ReactComponentLifeCycle', () => {
it('should allow update state inside of componentWillMount', () => {
class StatefulComponent extends React.Component {
componentWillMount() {
UNSAFE_componentWillMount() {
this.setState({stateField: 'something'});
}
@@ -231,7 +231,7 @@ describe('ReactComponentLifeCycle', () => {
// reaching into the updater.
return this.updater.isMounted(this);
}
componentWillMount() {
UNSAFE_componentWillMount() {
expect(this._isMounted()).toBeFalsy();
}
componentDidMount() {
@@ -258,7 +258,7 @@ describe('ReactComponentLifeCycle', () => {
// reaching into the updater.
return this.updater.isMounted(this);
}
componentWillMount() {
UNSAFE_componentWillMount() {
expect(this._isMounted()).toBeFalsy();
}
componentDidMount() {
@@ -334,7 +334,7 @@ describe('ReactComponentLifeCycle', () => {
this.state = initState;
}
componentWillMount() {
UNSAFE_componentWillMount() {
this._testJournal.stateAtStartOfWillMount = clone(this.state);
this._testJournal.lifeCycleAtStartOfWillMount = getLifeCycleState(this);
this.state.hasWillMountCompleted = true;
@@ -509,11 +509,18 @@ describe('ReactComponentLifeCycle', () => {
};
};
class Outer extends React.Component {
componentWillMount = logger('outer componentWillMount');
state = {};
static getDerivedStateFromProps(props, prevState) {
log.push('outer getDerivedStateFromProps');
return null;
}
UNSAFE_componentWillMount = logger('outer componentWillMount');
componentDidMount = logger('outer componentDidMount');
componentWillReceiveProps = logger('outer componentWillReceiveProps');
UNSAFE_componentWillReceiveProps = logger(
'outer componentWillReceiveProps',
);
shouldComponentUpdate = logger('outer shouldComponentUpdate');
componentWillUpdate = logger('outer componentWillUpdate');
UNSAFE_componentWillUpdate = logger('outer componentWillUpdate');
componentDidUpdate = logger('outer componentDidUpdate');
componentWillUnmount = logger('outer componentWillUnmount');
render() {
@@ -526,11 +533,18 @@ describe('ReactComponentLifeCycle', () => {
}
class Inner extends React.Component {
componentWillMount = logger('inner componentWillMount');
state = {};
static getDerivedStateFromProps(props, prevState) {
log.push('inner getDerivedStateFromProps');
return null;
}
UNSAFE_componentWillMount = logger('inner componentWillMount');
componentDidMount = logger('inner componentDidMount');
componentWillReceiveProps = logger('inner componentWillReceiveProps');
UNSAFE_componentWillReceiveProps = logger(
'inner componentWillReceiveProps',
);
shouldComponentUpdate = logger('inner shouldComponentUpdate');
componentWillUpdate = logger('inner componentWillUpdate');
UNSAFE_componentWillUpdate = logger('inner componentWillUpdate');
componentDidUpdate = logger('inner componentDidUpdate');
componentWillUnmount = logger('inner componentWillUnmount');
render() {
@@ -540,21 +554,33 @@ describe('ReactComponentLifeCycle', () => {
const container = document.createElement('div');
log = [];
ReactDOM.render(<Outer x={17} />, container);
expect(() => ReactDOM.render(<Outer x={1} />, 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().',
]);
expect(log).toEqual([
'outer getDerivedStateFromProps',
'outer componentWillMount',
'inner getDerivedStateFromProps',
'inner componentWillMount',
'inner componentDidMount',
'outer componentDidMount',
]);
// Dedup warnings
log = [];
ReactDOM.render(<Outer x={42} />, container);
ReactDOM.render(<Outer x={2} />, container);
expect(log).toEqual([
'outer componentWillReceiveProps',
'outer getDerivedStateFromProps',
'outer shouldComponentUpdate',
'outer componentWillUpdate',
'inner componentWillReceiveProps',
'inner getDerivedStateFromProps',
'inner shouldComponentUpdate',
'inner componentWillUpdate',
'inner componentDidUpdate',
@@ -579,7 +605,7 @@ describe('ReactComponentLifeCycle', () => {
log.push('render');
return <Child />;
},
componentWillMount() {
UNSAFE_componentWillMount() {
log.push('will mount');
},
componentDidMount() {
@@ -619,4 +645,43 @@ describe('ReactComponentLifeCycle', () => {
'ref',
]);
});
it('should warn if getDerivedStateFromProps returns undefined', () => {
class MyComponent extends React.Component {
state = {};
static getDerivedStateFromProps() {}
render() {
return null;
}
}
const div = document.createElement('div');
expect(() => ReactDOM.render(<MyComponent />, div)).toWarnDev(
'MyComponent.getDerivedStateFromProps(): A valid state object (or null) must ' +
'be returned. You have returned undefined.',
);
// De-duped
ReactDOM.render(<MyComponent />, div);
});
it('should warn if state is not initialized before getDerivedStateFromProps', () => {
class MyComponent extends React.Component {
static getDerivedStateFromProps() {
return null;
}
render() {
return null;
}
}
const div = document.createElement('div');
expect(() => ReactDOM.render(<MyComponent />, div)).toWarnDev(
'MyComponent: Did not properly initialize state during construction. ' +
'Expected state to be an object, but it was undefined.',
);
// De-duped
ReactDOM.render(<MyComponent />, div);
});
});
@@ -850,7 +850,7 @@ describe('ReactCompositeComponent', () => {
foo: PropTypes.string.isRequired,
};
componentWillReceiveProps(nextProps, nextContext) {
UNSAFE_componentWillReceiveProps(nextProps, nextContext) {
expect('foo' in nextContext).toBe(true);
}
@@ -865,7 +865,7 @@ describe('ReactCompositeComponent', () => {
}
class Intermediary extends React.Component {
componentWillReceiveProps(nextProps, nextContext) {
UNSAFE_componentWillReceiveProps(nextProps, nextContext) {
expect('foo' in nextContext).toBe(false);
}
@@ -916,7 +916,7 @@ describe('ReactCompositeComponent', () => {
foo: PropTypes.string.isRequired,
};
componentWillReceiveProps(nextProps, nextContext) {
UNSAFE_componentWillReceiveProps(nextProps, nextContext) {
expect('foo' in nextContext).toBe(true);
if (nextProps !== this.props) {
@@ -938,7 +938,7 @@ describe('ReactCompositeComponent', () => {
foo: PropTypes.string.isRequired,
};
componentWillReceiveProps(nextProps, nextContext) {
UNSAFE_componentWillReceiveProps(nextProps, nextContext) {
expect('foo' in nextContext).toBe(true);
if (nextProps !== this.props) {
@@ -956,7 +956,7 @@ describe('ReactCompositeComponent', () => {
}
class ChildWithoutContext extends React.Component {
componentWillReceiveProps(nextProps, nextContext) {
UNSAFE_componentWillReceiveProps(nextProps, nextContext) {
expect('foo' in nextContext).toBe(false);
if (nextProps !== this.props) {
@@ -1047,7 +1047,7 @@ describe('ReactCompositeComponent', () => {
class Component extends React.Component {
state = {updated: false};
componentWillReceiveProps(props) {
UNSAFE_componentWillReceiveProps(props) {
expect(props.update).toBe(1);
expect(renders).toBe(1);
this.setState({updated: true});
@@ -1075,7 +1075,7 @@ describe('ReactCompositeComponent', () => {
class Component extends React.Component {
state = {updated: false};
componentWillReceiveProps(props) {
UNSAFE_componentWillReceiveProps(props) {
expect(props.update).toBe(1);
expect(renders).toBe(1);
this.setState({updated: true});
@@ -1377,7 +1377,7 @@ describe('ReactCompositeComponent', () => {
const log = [];
class Spy extends React.Component {
componentWillMount() {
UNSAFE_componentWillMount() {
log.push(this.props.name + ' componentWillMount');
}
render() {
@@ -1556,7 +1556,7 @@ describe('ReactCompositeComponent', () => {
};
}
componentWillMount() {
UNSAFE_componentWillMount() {
this.setState(
{hasUpdatedState: true},
() => (stateSuccessfullyUpdated = this.state.hasUpdatedState),
@@ -1586,7 +1586,7 @@ describe('ReactCompositeComponent', () => {
};
}
componentWillMount() {
UNSAFE_componentWillMount() {
instance = this;
}
@@ -46,7 +46,7 @@ describe('ReactCompositeComponent-state', () => {
return <div>{this.state.color}</div>;
}
componentWillMount() {
UNSAFE_componentWillMount() {
this.peekAtState('componentWillMount-start');
this.setState(function(state) {
this.peekAtState('before-setState-sunrise', state);
@@ -78,7 +78,7 @@ describe('ReactCompositeComponent-state', () => {
this.peekAtState('componentDidMount-end');
}
componentWillReceiveProps(newProps) {
UNSAFE_componentWillReceiveProps(newProps) {
this.peekAtState('componentWillReceiveProps-start');
if (newProps.nextColor) {
this.setState(function(state) {
@@ -105,7 +105,7 @@ describe('ReactCompositeComponent-state', () => {
return true;
}
componentWillUpdate(nextProps, nextState) {
UNSAFE_componentWillUpdate(nextProps, nextState) {
this.peekAtState('componentWillUpdate-currentState');
this.peekAtState('componentWillUpdate-nextState', nextState);
}
@@ -323,7 +323,7 @@ describe('ReactCompositeComponent-state', () => {
}
let updated = false;
class Child extends React.Component {
componentWillReceiveProps() {
UNSAFE_componentWillReceiveProps() {
if (updated) {
return;
}
@@ -383,7 +383,7 @@ describe('ReactCompositeComponent-state', () => {
let ops = [];
class Test extends React.Component {
state = {step: 1, extra: true};
componentWillReceiveProps() {
UNSAFE_componentWillReceiveProps() {
this.setState({step: 2}, () => {
// Tests that earlier setState callbacks are not dropped
ops.push(
@@ -426,7 +426,7 @@ describe('ReactCompositeComponent-state', () => {
let ops = [];
class Test extends React.Component {
state = {step: 1, extra: true};
componentWillMount() {
UNSAFE_componentWillMount() {
this.setState({step: 2}, () => {
// Tests that earlier setState callbacks are not dropped
ops.push(
@@ -458,4 +458,50 @@ describe('ReactCompositeComponent-state', () => {
'callback -- step: 3, extra: false',
]);
});
it('should support stateful module pattern components', () => {
function Child() {
return {
state: {
count: 123,
},
render() {
return <div>{`count:${this.state.count}`}</div>;
},
};
}
const el = document.createElement('div');
ReactDOM.render(<Child />, el);
expect(el.textContent).toBe('count:123');
});
it('should support getDerivedStateFromProps for module pattern components', () => {
function Child() {
return {
state: {
count: 1,
},
render() {
return <div>{`count:${this.state.count}`}</div>;
},
};
}
Child.getDerivedStateFromProps = (props, prevState) => {
return {
count: prevState.count + props.incrementBy,
};
};
const el = document.createElement('div');
ReactDOM.render(<Child incrementBy={0} />, el);
expect(el.textContent).toBe('count:1');
ReactDOM.render(<Child incrementBy={2} />, el);
expect(el.textContent).toBe('count:3');
ReactDOM.render(<Child incrementBy={1} />, el);
expect(el.textContent).toBe('count:4');
});
});
@@ -231,7 +231,7 @@ describe('ReactDOMServerIntegration', () => {
getChildContext() {
return {text: this.state.text};
}
componentWillMount() {
UNSAFE_componentWillMount() {
this.setState({text: 'foo'});
}
render() {
@@ -0,0 +1,43 @@
/**
* 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
*/
'use strict';
let React;
let ReactFeatureFlags;
let ReactDOMServer;
describe('ReactDOMServerLifecycles', () => {
beforeEach(() => {
ReactFeatureFlags = require('shared/ReactFeatureFlags');
ReactFeatureFlags.warnAboutDeprecatedLifecycles = true;
React = require('react');
ReactDOMServer = require('react-dom/server');
});
// TODO (RFC #6) Merge this back into ReactDOMServerLifecycles-test once
// the 'warnAboutDeprecatedLifecycles' feature flag has been removed.
it('should warn about deprecated lifecycle hooks', () => {
class Component extends React.Component {
componentWillMount() {}
render() {
return null;
}
}
expect(() => ReactDOMServer.renderToString(<Component />)).toWarnDev(
'Warning: Component: componentWillMount() is deprecated and will be removed ' +
'in the next major version.',
);
// De-duped
ReactDOMServer.renderToString(<Component />);
});
});
@@ -0,0 +1,157 @@
/**
* 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
*/
'use strict';
const ReactDOMServerIntegrationUtils = require('./utils/ReactDOMServerIntegrationTestUtils');
let React;
let ReactDOMServer;
function initModules() {
// Reset warning cache.
jest.resetModuleRegistry();
React = require('react');
ReactDOMServer = require('react-dom/server');
// Make them available to the helpers.
return {
ReactDOMServer,
};
}
const {resetModules} = ReactDOMServerIntegrationUtils(initModules);
describe('ReactDOMServerLifecycles', () => {
beforeEach(() => {
resetModules();
});
it('should invoke the correct lifecycle hooks', () => {
const log = [];
class Outer extends React.Component {
UNSAFE_componentWillMount() {
log.push('outer componentWillMount');
}
render() {
log.push('outer render');
return <Inner />;
}
}
class Inner extends React.Component {
UNSAFE_componentWillMount() {
log.push('inner componentWillMount');
}
render() {
log.push('inner render');
return null;
}
}
ReactDOMServer.renderToString(<Outer />);
expect(log).toEqual([
'outer componentWillMount',
'outer render',
'inner componentWillMount',
'inner render',
]);
});
it('should update instance.state with value returned from getDerivedStateFromProps', () => {
class Grandparent extends React.Component {
state = {
foo: 'foo',
};
render() {
return (
<div>
{`Grandparent: ${this.state.foo}`}
<Parent />
</div>
);
}
}
class Parent extends React.Component {
state = {
bar: 'bar',
baz: 'baz',
};
static getDerivedStateFromProps(props, prevState) {
return {
bar: `not ${prevState.bar}`,
};
}
render() {
return (
<div>
{`Parent: ${this.state.bar}, ${this.state.baz}`}
<Child />;
</div>
);
}
}
class Child extends React.Component {
state = {};
static getDerivedStateFromProps() {
return {
qux: 'qux',
};
}
render() {
return `Child: ${this.state.qux}`;
}
}
const markup = ReactDOMServer.renderToString(<Grandparent />);
expect(markup).toContain('Grandparent: foo');
expect(markup).toContain('Parent: not bar, baz');
expect(markup).toContain('Child: qux');
});
it('should warn if getDerivedStateFromProps returns undefined', () => {
class Component extends React.Component {
state = {};
static getDerivedStateFromProps() {}
render() {
return null;
}
}
expect(() => ReactDOMServer.renderToString(<Component />)).toWarnDev(
'Component.getDerivedStateFromProps(): A valid state object (or null) must ' +
'be returned. You have returned undefined.',
);
// De-duped
ReactDOMServer.renderToString(<Component />);
});
it('should warn if state is not initialized before getDerivedStateFromProps', () => {
class Component extends React.Component {
static getDerivedStateFromProps() {
return null;
}
render() {
return null;
}
}
expect(() => ReactDOMServer.renderToString(<Component />)).toWarnDev(
'Component: Did not properly initialize state during construction. ' +
'Expected state to be an object, but it was undefined.',
);
// De-duped
ReactDOMServer.renderToString(<Component />);
});
});
+38 -38
View File
@@ -50,16 +50,16 @@ describe('ReactErrorBoundaries', () => {
log.push('BrokenConstructor render');
return <div>{this.props.children}</div>;
}
componentWillMount() {
UNSAFE_componentWillMount() {
log.push('BrokenConstructor componentWillMount');
}
componentDidMount() {
log.push('BrokenConstructor componentDidMount');
}
componentWillReceiveProps() {
UNSAFE_componentWillReceiveProps() {
log.push('BrokenConstructor componentWillReceiveProps');
}
componentWillUpdate() {
UNSAFE_componentWillUpdate() {
log.push('BrokenConstructor componentWillUpdate');
}
componentDidUpdate() {
@@ -79,17 +79,17 @@ describe('ReactErrorBoundaries', () => {
log.push('BrokenComponentWillMount render');
return <div>{this.props.children}</div>;
}
componentWillMount() {
UNSAFE_componentWillMount() {
log.push('BrokenComponentWillMount componentWillMount [!]');
throw new Error('Hello');
}
componentDidMount() {
log.push('BrokenComponentWillMount componentDidMount');
}
componentWillReceiveProps() {
UNSAFE_componentWillReceiveProps() {
log.push('BrokenComponentWillMount componentWillReceiveProps');
}
componentWillUpdate() {
UNSAFE_componentWillUpdate() {
log.push('BrokenComponentWillMount componentWillUpdate');
}
componentDidUpdate() {
@@ -109,17 +109,17 @@ describe('ReactErrorBoundaries', () => {
log.push('BrokenComponentDidMount render');
return <div>{this.props.children}</div>;
}
componentWillMount() {
UNSAFE_componentWillMount() {
log.push('BrokenComponentDidMount componentWillMount');
}
componentDidMount() {
log.push('BrokenComponentDidMount componentDidMount [!]');
throw new Error('Hello');
}
componentWillReceiveProps() {
UNSAFE_componentWillReceiveProps() {
log.push('BrokenComponentDidMount componentWillReceiveProps');
}
componentWillUpdate() {
UNSAFE_componentWillUpdate() {
log.push('BrokenComponentDidMount componentWillUpdate');
}
componentDidUpdate() {
@@ -139,19 +139,19 @@ describe('ReactErrorBoundaries', () => {
log.push('BrokenComponentWillReceiveProps render');
return <div>{this.props.children}</div>;
}
componentWillMount() {
UNSAFE_componentWillMount() {
log.push('BrokenComponentWillReceiveProps componentWillMount');
}
componentDidMount() {
log.push('BrokenComponentWillReceiveProps componentDidMount');
}
componentWillReceiveProps() {
UNSAFE_componentWillReceiveProps() {
log.push(
'BrokenComponentWillReceiveProps componentWillReceiveProps [!]',
);
throw new Error('Hello');
}
componentWillUpdate() {
UNSAFE_componentWillUpdate() {
log.push('BrokenComponentWillReceiveProps componentWillUpdate');
}
componentDidUpdate() {
@@ -171,16 +171,16 @@ describe('ReactErrorBoundaries', () => {
log.push('BrokenComponentWillUpdate render');
return <div>{this.props.children}</div>;
}
componentWillMount() {
UNSAFE_componentWillMount() {
log.push('BrokenComponentWillUpdate componentWillMount');
}
componentDidMount() {
log.push('BrokenComponentWillUpdate componentDidMount');
}
componentWillReceiveProps() {
UNSAFE_componentWillReceiveProps() {
log.push('BrokenComponentWillUpdate componentWillReceiveProps');
}
componentWillUpdate() {
UNSAFE_componentWillUpdate() {
log.push('BrokenComponentWillUpdate componentWillUpdate [!]');
throw new Error('Hello');
}
@@ -204,16 +204,16 @@ describe('ReactErrorBoundaries', () => {
log.push('BrokenComponentDidUpdate render');
return <div>{this.props.children}</div>;
}
componentWillMount() {
UNSAFE_componentWillMount() {
log.push('BrokenComponentDidUpdate componentWillMount');
}
componentDidMount() {
log.push('BrokenComponentDidUpdate componentDidMount');
}
componentWillReceiveProps() {
UNSAFE_componentWillReceiveProps() {
log.push('BrokenComponentDidUpdate componentWillReceiveProps');
}
componentWillUpdate() {
UNSAFE_componentWillUpdate() {
log.push('BrokenComponentDidUpdate componentWillUpdate');
}
componentDidUpdate() {
@@ -237,16 +237,16 @@ describe('ReactErrorBoundaries', () => {
log.push('BrokenComponentWillUnmount render');
return <div>{this.props.children}</div>;
}
componentWillMount() {
UNSAFE_componentWillMount() {
log.push('BrokenComponentWillUnmount componentWillMount');
}
componentDidMount() {
log.push('BrokenComponentWillUnmount componentDidMount');
}
componentWillReceiveProps() {
UNSAFE_componentWillReceiveProps() {
log.push('BrokenComponentWillUnmount componentWillReceiveProps');
}
componentWillUpdate() {
UNSAFE_componentWillUpdate() {
log.push('BrokenComponentWillUnmount componentWillUpdate');
}
componentDidUpdate() {
@@ -272,7 +272,7 @@ describe('ReactErrorBoundaries', () => {
log.push('BrokenComponentWillMountErrorBoundary render success');
return <div>{this.props.children}</div>;
}
componentWillMount() {
UNSAFE_componentWillMount() {
log.push(
'BrokenComponentWillMountErrorBoundary componentWillMount [!]',
);
@@ -304,7 +304,7 @@ describe('ReactErrorBoundaries', () => {
log.push('BrokenComponentDidMountErrorBoundary render success');
return <div>{this.props.children}</div>;
}
componentWillMount() {
UNSAFE_componentWillMount() {
log.push('BrokenComponentDidMountErrorBoundary componentWillMount');
}
componentDidMount() {
@@ -334,7 +334,7 @@ describe('ReactErrorBoundaries', () => {
log.push('BrokenRenderErrorBoundary render success');
return <div>{this.props.children}</div>;
}
componentWillMount() {
UNSAFE_componentWillMount() {
log.push('BrokenRenderErrorBoundary componentWillMount');
}
componentDidMount() {
@@ -358,16 +358,16 @@ describe('ReactErrorBoundaries', () => {
log.push('BrokenRender render [!]');
throw new Error('Hello');
}
componentWillMount() {
UNSAFE_componentWillMount() {
log.push('BrokenRender componentWillMount');
}
componentDidMount() {
log.push('BrokenRender componentDidMount');
}
componentWillReceiveProps() {
UNSAFE_componentWillReceiveProps() {
log.push('BrokenRender componentWillReceiveProps');
}
componentWillUpdate() {
UNSAFE_componentWillUpdate() {
log.push('BrokenRender componentWillUpdate');
}
componentDidUpdate() {
@@ -387,7 +387,7 @@ describe('ReactErrorBoundaries', () => {
log.push('NoopErrorBoundary render');
return <BrokenRender />;
}
componentWillMount() {
UNSAFE_componentWillMount() {
log.push('NoopErrorBoundary componentWillMount');
}
componentDidMount() {
@@ -413,16 +413,16 @@ describe('ReactErrorBoundaries', () => {
log.push(`${this.props.logName} render`);
return <div>{this.props.children}</div>;
}
componentWillMount() {
UNSAFE_componentWillMount() {
log.push(`${this.props.logName} componentWillMount`);
}
componentDidMount() {
log.push(`${this.props.logName} componentDidMount`);
}
componentWillReceiveProps() {
UNSAFE_componentWillReceiveProps() {
log.push(`${this.props.logName} componentWillReceiveProps`);
}
componentWillUpdate() {
UNSAFE_componentWillUpdate() {
log.push(`${this.props.logName} componentWillUpdate`);
}
componentDidUpdate() {
@@ -451,16 +451,16 @@ describe('ReactErrorBoundaries', () => {
log.push(`${this.props.logName} componentDidCatch`);
this.setState({error});
}
componentWillMount() {
UNSAFE_componentWillMount() {
log.push(`${this.props.logName} componentWillMount`);
}
componentDidMount() {
log.push(`${this.props.logName} componentDidMount`);
}
componentWillReceiveProps() {
UNSAFE_componentWillReceiveProps() {
log.push(`${this.props.logName} componentWillReceiveProps`);
}
componentWillUpdate() {
UNSAFE_componentWillUpdate() {
log.push(`${this.props.logName} componentWillUpdate`);
}
componentDidUpdate() {
@@ -490,7 +490,7 @@ describe('ReactErrorBoundaries', () => {
log.push('RetryErrorBoundary render');
return <BrokenRender />;
}
componentWillMount() {
UNSAFE_componentWillMount() {
log.push('RetryErrorBoundary componentWillMount');
}
componentDidMount() {
@@ -511,7 +511,7 @@ describe('ReactErrorBoundaries', () => {
super(props);
log.push('ErrorMessage constructor');
}
componentWillMount() {
UNSAFE_componentWillMount() {
log.push('ErrorMessage componentWillMount');
}
componentDidMount() {
@@ -703,7 +703,7 @@ describe('ReactErrorBoundaries', () => {
render() {
return <div>{this.props.children}</div>;
}
componentWillMount() {
UNSAFE_componentWillMount() {
throw new Error('Hello');
}
}
@@ -727,7 +727,7 @@ describe('ReactErrorBoundaries', () => {
render() {
return <div>{this.props.children}</div>;
},
componentWillMount() {
UNSAFE_componentWillMount() {
throw new Error('Hello');
},
};
+1 -1
View File
@@ -330,7 +330,7 @@ describe('ReactMultiChild', () => {
const log = [];
class Spy extends React.Component {
componentWillMount() {
UNSAFE_componentWillMount() {
log.push(this.props.name + ' componentWillMount');
}
render() {
+11 -11
View File
@@ -98,7 +98,7 @@ describe('ReactDOMServer', () => {
this.state = {name: 'TestComponent'};
}
componentWillMount() {
UNSAFE_componentWillMount() {
lifecycle.push('componentWillMount');
}
@@ -111,7 +111,7 @@ describe('ReactDOMServer', () => {
return <span>Component name: {this.state.name}</span>;
}
componentWillUpdate() {
UNSAFE_componentWillUpdate() {
lifecycle.push('componentWillUpdate');
}
@@ -123,7 +123,7 @@ describe('ReactDOMServer', () => {
lifecycle.push('shouldComponentUpdate');
}
componentWillReceiveProps() {
UNSAFE_componentWillReceiveProps() {
lifecycle.push('componentWillReceiveProps');
}
@@ -240,7 +240,7 @@ describe('ReactDOMServer', () => {
this.state = {name: 'TestComponent'};
}
componentWillMount() {
UNSAFE_componentWillMount() {
lifecycle.push('componentWillMount');
}
@@ -253,7 +253,7 @@ describe('ReactDOMServer', () => {
return <span>Component name: {this.state.name}</span>;
}
componentWillUpdate() {
UNSAFE_componentWillUpdate() {
lifecycle.push('componentWillUpdate');
}
@@ -265,7 +265,7 @@ describe('ReactDOMServer', () => {
lifecycle.push('shouldComponentUpdate');
}
componentWillReceiveProps() {
UNSAFE_componentWillReceiveProps() {
lifecycle.push('componentWillReceiveProps');
}
@@ -297,7 +297,7 @@ describe('ReactDOMServer', () => {
it('allows setState in componentWillMount without using DOM', () => {
class Component extends React.Component {
componentWillMount() {
UNSAFE_componentWillMount() {
this.setState({text: 'hello, world'});
}
@@ -316,7 +316,7 @@ describe('ReactDOMServer', () => {
this.state = {text: 'default state'};
}
componentWillMount() {
UNSAFE_componentWillMount() {
this.setState({text: 'hello, world'});
}
@@ -397,7 +397,7 @@ describe('ReactDOMServer', () => {
}
class Component extends React.Component {
componentWillMount() {
UNSAFE_componentWillMount() {
this.setState({text: 'hello, world'});
}
@@ -420,7 +420,7 @@ describe('ReactDOMServer', () => {
it('warns with a no-op when an async setState is triggered', () => {
class Foo extends React.Component {
componentWillMount() {
UNSAFE_componentWillMount() {
this.setState({text: 'hello'});
setTimeout(() => {
this.setState({text: 'error'});
@@ -446,7 +446,7 @@ describe('ReactDOMServer', () => {
it('warns with a no-op when an async forceUpdate is triggered', () => {
class Baz extends React.Component {
componentWillMount() {
UNSAFE_componentWillMount() {
this.forceUpdate();
setTimeout(() => {
this.forceUpdate();
@@ -98,6 +98,22 @@ describe('ReactStatelessComponent', () => {
expect(el.textContent).toBe('mest');
});
it('should warn for getDerivedStateFromProps on a functional component', () => {
function StatelessComponentWithChildContext() {
return null;
}
StatelessComponentWithChildContext.getDerivedStateFromProps = function() {};
const container = document.createElement('div');
expect(() =>
ReactDOM.render(<StatelessComponentWithChildContext />, container),
).toWarnDev(
'StatelessComponentWithChildContext: Stateless ' +
'functional components do not support getDerivedStateFromProps.',
);
});
it('should warn for childContextTypes on a functional component', () => {
function StatelessComponentWithChildContext(props) {
return <div>{props.name}</div>;
+9 -9
View File
@@ -399,7 +399,7 @@ describe('ReactUpdates', () => {
let didUpdates = [];
const UpdateLoggingMixin = {
componentWillUpdate: function() {
UNSAFE_componentWillUpdate: function() {
willUpdates.push(this.constructor.displayName);
},
componentDidUpdate: function() {
@@ -723,7 +723,7 @@ describe('ReactUpdates', () => {
return <div />;
}
componentWillUpdate() {
UNSAFE_componentWillUpdate() {
x.go();
}
}
@@ -746,7 +746,7 @@ describe('ReactUpdates', () => {
class A extends React.Component {
state = {x: 0};
componentWillMount() {
UNSAFE_componentWillMount() {
a = this;
}
@@ -756,7 +756,7 @@ describe('ReactUpdates', () => {
}
class B extends React.Component {
componentWillMount() {
UNSAFE_componentWillMount() {
a.setState({x: 1});
}
@@ -784,7 +784,7 @@ describe('ReactUpdates', () => {
class A extends React.Component {
state = {x: this.props.x};
componentWillReceiveProps(nextProps) {
UNSAFE_componentWillReceiveProps(nextProps) {
const newX = nextProps.x;
this.setState({x: newX}, function() {
// State should have updated by the time this callback gets called
@@ -945,7 +945,7 @@ describe('ReactUpdates', () => {
class Child extends React.Component {
state = {updated: false};
componentWillUpdate() {
UNSAFE_componentWillUpdate() {
if (!once) {
once = true;
this.setState({updated: true});
@@ -1100,7 +1100,7 @@ describe('ReactUpdates', () => {
let ops = [];
class Foo extends React.Component {
state = {a: false, b: false};
componentWillUpdate(_, nextState) {
UNSAFE_componentWillUpdate(_, nextState) {
if (!nextState.a) {
this.setState({a: true});
}
@@ -1143,7 +1143,7 @@ describe('ReactUpdates', () => {
let ops = [];
class Foo extends React.Component {
state = {a: false};
componentWillUpdate(_, nextState) {
UNSAFE_componentWillUpdate(_, nextState) {
if (!nextState.a) {
this.setState({a: true});
}
@@ -1316,7 +1316,7 @@ describe('ReactUpdates', () => {
componentDidMount() {
this.setState({step: 1});
}
componentWillUpdate() {
UNSAFE_componentWillUpdate() {
this.setState({step: 2});
}
render() {
+1 -1
View File
@@ -88,7 +88,7 @@ describe('findDOMNode', () => {
it('findDOMNode should not throw an error when called within a component that is not mounted', () => {
class Bar extends React.Component {
componentWillMount() {
UNSAFE_componentWillMount() {
expect(ReactDOM.findDOMNode(this)).toBeNull();
}
+75 -3
View File
@@ -19,6 +19,7 @@ import warning from 'fbjs/lib/warning';
import checkPropTypes from 'prop-types/checkPropTypes';
import describeComponentFrame from 'shared/describeComponentFrame';
import {ReactDebugCurrentFrame} from 'shared/ReactGlobalSharedState';
import {warnAboutDeprecatedLifecycles} from 'shared/ReactFeatureFlags';
import {
REACT_FRAGMENT_TYPE,
REACT_CALL_TYPE,
@@ -123,6 +124,9 @@ let didWarnDefaultTextareaValue = false;
let didWarnInvalidOptionChildren = false;
const didWarnAboutNoopUpdateForComponent = {};
const didWarnAboutBadClass = {};
const didWarnAboutDeprecatedWillMount = {};
const didWarnAboutUndefinedDerivedState = {};
const didWarnAboutUninitializedState = {};
const valuePropNames = ['value', 'defaultValue'];
const newlineEatingTags = {
listing: true,
@@ -421,6 +425,49 @@ function resolve(
if (shouldConstruct(Component)) {
inst = new Component(element.props, publicContext, updater);
if (typeof Component.getDerivedStateFromProps === 'function') {
if (__DEV__) {
if (inst.state === null || inst.state === undefined) {
const componentName = getComponentName(Component) || '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,
inst.state === null ? 'null' : 'undefined',
);
didWarnAboutUninitializedState[componentName] = true;
}
}
}
partialState = Component.getDerivedStateFromProps.call(
null,
element.props,
inst.state,
);
if (__DEV__) {
if (partialState === undefined) {
const componentName = getComponentName(Component) || 'Unknown';
if (!didWarnAboutUndefinedDerivedState[componentName]) {
warning(
false,
'%s.getDerivedStateFromProps(): A valid state object (or null) must be returned. ' +
'You have returned undefined.',
componentName,
);
didWarnAboutUndefinedDerivedState[componentName] = true;
}
}
}
if (partialState != null) {
inst.state = Object.assign({}, inst.state, partialState);
}
}
} else {
if (__DEV__) {
if (
@@ -457,8 +504,33 @@ function resolve(
if (initialState === undefined) {
inst.state = initialState = null;
}
if (inst.componentWillMount) {
inst.componentWillMount();
if (inst.UNSAFE_componentWillMount || inst.componentWillMount) {
if (inst.componentWillMount) {
if (__DEV__) {
if (warnAboutDeprecatedLifecycles) {
const componentName = getComponentName(Component) || 'Unknown';
if (!didWarnAboutDeprecatedWillMount[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,
);
didWarnAboutDeprecatedWillMount[componentName] = true;
}
}
}
inst.componentWillMount();
} else {
inst.UNSAFE_componentWillMount();
}
if (queue.length) {
oldQueue = queue;
oldReplace = replace;
@@ -476,7 +548,7 @@ function resolve(
typeof partial === 'function'
? partial.call(inst, nextState, element.props, publicContext)
: partial;
if (partialState) {
if (partialState != null) {
if (dontMutate) {
dontMutate = false;
nextState = Object.assign({}, nextState, partialState);
+9 -1
View File
@@ -187,7 +187,9 @@ if (__DEV__) {
const NativeMethodsMixin_DEV = (NativeMethodsMixin: any);
invariant(
!NativeMethodsMixin_DEV.componentWillMount &&
!NativeMethodsMixin_DEV.componentWillReceiveProps,
!NativeMethodsMixin_DEV.componentWillReceiveProps &&
!NativeMethodsMixin_DEV.UNSAFE_componentWillMount &&
!NativeMethodsMixin_DEV.UNSAFE_componentWillReceiveProps,
'Do not override existing functions.',
);
NativeMethodsMixin_DEV.componentWillMount = function() {
@@ -196,6 +198,12 @@ if (__DEV__) {
NativeMethodsMixin_DEV.componentWillReceiveProps = function(newProps) {
throwOnStylesProp(this, newProps);
};
NativeMethodsMixin_DEV.UNSAFE_componentWillMount = function() {
throwOnStylesProp(this, this.props);
};
NativeMethodsMixin_DEV.UNSAFE_componentWillReceiveProps = function(newProps) {
throwOnStylesProp(this, newProps);
};
}
export default NativeMethodsMixin;
+45 -4
View File
@@ -60,12 +60,14 @@ import {
import {NoWork, Never} from './ReactFiberExpirationTime';
import {AsyncUpdates} from './ReactTypeOfInternalContext';
let warnedAboutStatelessRefs;
let didWarnAboutBadClass;
let didWarnAboutGetDerivedStateOnFunctionalComponent;
let didWarnAboutStatelessRefs;
if (__DEV__) {
warnedAboutStatelessRefs = {};
didWarnAboutBadClass = {};
didWarnAboutGetDerivedStateOnFunctionalComponent = {};
didWarnAboutStatelessRefs = {};
}
export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
@@ -87,6 +89,7 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
const {
adoptClassInstance,
callGetDerivedStateFromProps,
constructClassInstance,
mountClassInstance,
// resumeMountClassInstance,
@@ -483,9 +486,30 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
value !== null &&
typeof value.render === 'function'
) {
const Component = workInProgress.type;
// Proceed under the assumption that this is a class instance
workInProgress.tag = ClassComponent;
workInProgress.memoizedState =
value.state !== null && value.state !== undefined ? value.state : null;
if (typeof Component.getDerivedStateFromProps === 'function') {
const partialState = callGetDerivedStateFromProps(
workInProgress,
value,
props,
);
if (partialState !== null && partialState !== undefined) {
workInProgress.memoizedState = Object.assign(
{},
workInProgress.memoizedState,
partialState,
);
}
}
// Push context providers early to prevent context stack mismatches.
// During mounting we don't know the child context yet as the instance doesn't exist.
// We will invalidate the child context in finishClassComponent() right after rendering.
@@ -518,8 +542,8 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
if (debugSource) {
warningKey = debugSource.fileName + ':' + debugSource.lineNumber;
}
if (!warnedAboutStatelessRefs[warningKey]) {
warnedAboutStatelessRefs[warningKey] = true;
if (!didWarnAboutStatelessRefs[warningKey]) {
didWarnAboutStatelessRefs[warningKey] = true;
warning(
false,
'Stateless function components cannot be given refs. ' +
@@ -529,6 +553,23 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
);
}
}
if (typeof fn.getDerivedStateFromProps === 'function') {
const componentName = getComponentName(workInProgress) || 'Unknown';
if (
!didWarnAboutGetDerivedStateOnFunctionalComponent[componentName]
) {
warning(
false,
'%s: Stateless functional components do not support getDerivedStateFromProps.',
componentName,
);
didWarnAboutGetDerivedStateOnFunctionalComponent[
componentName
] = true;
}
}
}
reconcileChildren(current, workInProgress, value);
memoizeProps(workInProgress, props);
+240 -17
View File
@@ -14,6 +14,7 @@ import {Update} from 'shared/ReactTypeOfSideEffect';
import {
debugRenderPhaseSideEffects,
enableAsyncSubtreeAPI,
warnAboutDeprecatedLifecycles,
} from 'shared/ReactFeatureFlags';
import {isMounted} from 'react-reconciler/reflection';
import * as ReactInstanceMap from 'shared/ReactInstanceMap';
@@ -40,12 +41,27 @@ import {hasContextChanged} from './ReactFiberContext';
const fakeInternalInstance = {};
const isArray = Array.isArray;
let didWarnAboutLegacyWillMount;
let didWarnAboutLegacyWillReceiveProps;
let didWarnAboutLegacyWillUpdate;
let didWarnAboutStateAssignmentForComponent;
let didWarnAboutUndefinedDerivedState;
let didWarnAboutUninitializedState;
let didWarnAboutWillReceivePropsAndDerivedState;
let warnOnInvalidCallback;
if (__DEV__) {
const didWarnOnInvalidCallback = {};
if (warnAboutDeprecatedLifecycles) {
didWarnAboutLegacyWillMount = {};
didWarnAboutLegacyWillReceiveProps = {};
didWarnAboutLegacyWillUpdate = {};
}
didWarnAboutStateAssignmentForComponent = {};
didWarnAboutUndefinedDerivedState = {};
didWarnAboutUninitializedState = {};
didWarnAboutWillReceivePropsAndDerivedState = {};
const didWarnOnInvalidCallback = {};
warnOnInvalidCallback = function(callback: mixed, callerName: string) {
if (callback === null || typeof callback === 'function') {
@@ -319,6 +335,14 @@ export default function(
'componentWillRecieveProps(). Did you mean componentWillReceiveProps()?',
name,
);
const noUnsafeComponentWillRecieveProps =
typeof instance.UNSAFE_componentWillRecieveProps !== 'function';
warning(
noUnsafeComponentWillRecieveProps,
'%s has a method called ' +
'UNSAFE_componentWillRecieveProps(). Did you mean UNSAFE_componentWillReceiveProps()?',
name,
);
const hasMutatedProps = instance.props !== workInProgress.pendingProps;
warning(
instance.props === undefined || !hasMutatedProps,
@@ -378,8 +402,50 @@ export default function(
? getMaskedContext(workInProgress, unmaskedContext)
: emptyObject;
const instance = new ctor(props, context);
const state =
instance.state !== null && instance.state !== undefined
? instance.state
: null;
adoptClassInstance(workInProgress, instance);
if (__DEV__) {
if (
typeof ctor.getDerivedStateFromProps === 'function' &&
state === null
) {
const componentName = getComponentName(workInProgress) || '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,
instance.state === null ? 'null' : 'undefined',
);
didWarnAboutUninitializedState[componentName] = true;
}
}
}
workInProgress.memoizedState = state;
const partialState = callGetDerivedStateFromProps(
workInProgress,
instance,
props,
);
if (partialState !== null && partialState !== undefined) {
// Render-phase updates (like this) should not be added to the update queue,
// So that multiple render passes do not enqueue multiple updates.
// Instead, just synchronously merge the returned state into the instance.
workInProgress.memoizedState = Object.assign(
{},
workInProgress.memoizedState,
partialState,
);
}
// Cache unmasked context so we can avoid recreating masked context unless necessary.
// ReactFiberContext usually updates this cache but can't for newly-created instances.
if (needsContext) {
@@ -392,7 +458,32 @@ export default function(
function callComponentWillMount(workInProgress, instance) {
startPhaseTimer(workInProgress, 'componentWillMount');
const oldState = instance.state;
instance.componentWillMount();
if (typeof instance.componentWillMount === 'function') {
if (__DEV__) {
if (warnAboutDeprecatedLifecycles) {
const componentName = getComponentName(workInProgress) || 'Component';
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;
}
}
}
instance.componentWillMount();
} else {
instance.UNSAFE_componentWillMount();
}
stopPhaseTimer();
if (oldState !== instance.state) {
@@ -415,14 +506,41 @@ export default function(
newProps,
newContext,
) {
startPhaseTimer(workInProgress, 'componentWillReceiveProps');
const oldState = instance.state;
instance.componentWillReceiveProps(newProps, newContext);
stopPhaseTimer();
if (typeof instance.componentWillReceiveProps === 'function') {
if (__DEV__) {
if (warnAboutDeprecatedLifecycles) {
const componentName = getComponentName(workInProgress) || 'Component';
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;
}
}
}
// Simulate an async bailout/interruption by invoking lifecycle twice.
if (debugRenderPhaseSideEffects) {
startPhaseTimer(workInProgress, 'componentWillReceiveProps');
instance.componentWillReceiveProps(newProps, newContext);
stopPhaseTimer();
} else {
startPhaseTimer(workInProgress, 'componentWillReceiveProps');
instance.UNSAFE_componentWillReceiveProps(newProps, newContext);
stopPhaseTimer();
// Simulate an async bailout/interruption by invoking lifecycle twice.
if (debugRenderPhaseSideEffects) {
instance.UNSAFE_componentWillReceiveProps(newProps, newContext);
}
}
if (instance.state !== oldState) {
@@ -443,6 +561,58 @@ export default function(
}
}
function callGetDerivedStateFromProps(
workInProgress: Fiber,
instance: any,
props: any,
) {
const {type} = workInProgress;
if (typeof type.getDerivedStateFromProps === 'function') {
if (__DEV__) {
if (
typeof instance.componentWillReceiveProps === 'function' ||
typeof instance.UNSAFE_componentWillReceiveProps === 'function'
) {
const componentName = getComponentName(workInProgress) || 'Unknown';
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,
workInProgress.memoizedState,
);
if (__DEV__) {
if (partialState === undefined) {
const componentName = getComponentName(workInProgress) || 'Unknown';
if (!didWarnAboutUndefinedDerivedState[componentName]) {
warning(
false,
'%s.getDerivedStateFromProps(): A valid state object (or null) must be returned. ' +
'You have returned undefined.',
componentName,
);
didWarnAboutUndefinedDerivedState[componentName] = componentName;
}
}
}
return partialState;
}
}
// Invokes the mount life-cycles on a previously never rendered instance.
function mountClassInstance(
workInProgress: Fiber,
@@ -455,12 +625,11 @@ export default function(
}
const instance = workInProgress.stateNode;
const state = instance.state || null;
const props = workInProgress.pendingProps;
const unmaskedContext = getUnmaskedContext(workInProgress);
instance.props = props;
instance.state = workInProgress.memoizedState = state;
instance.state = workInProgress.memoizedState;
instance.refs = emptyObject;
instance.context = getMaskedContext(workInProgress, unmaskedContext);
@@ -473,7 +642,10 @@ export default function(
workInProgress.internalContextTag |= AsyncUpdates;
}
if (typeof instance.componentWillMount === 'function') {
if (
typeof instance.UNSAFE_componentWillMount === 'function' ||
typeof instance.componentWillMount === 'function'
) {
callComponentWillMount(workInProgress, instance);
// If we had additional state updates during this life-cycle, let's
// process them now.
@@ -619,7 +791,8 @@ export default function(
// during componentDidUpdate we pass the "current" props.
if (
typeof instance.componentWillReceiveProps === 'function' &&
(typeof instance.UNSAFE_componentWillReceiveProps === 'function' ||
typeof instance.componentWillReceiveProps === 'function') &&
(oldProps !== newProps || oldContext !== newContext)
) {
callComponentWillReceiveProps(
@@ -630,6 +803,15 @@ export default function(
);
}
let partialState;
if (oldProps !== newProps) {
partialState = callGetDerivedStateFromProps(
workInProgress,
instance,
newProps,
);
}
// Compute the next state using the memoized state and the update queue.
const oldState = workInProgress.memoizedState;
// TODO: Previous state can be null.
@@ -647,6 +829,16 @@ export default function(
newState = oldState;
}
if (partialState !== null && partialState !== undefined) {
// Render-phase updates (like this) should not be added to the update queue,
// So that multiple render passes do not enqueue multiple updates.
// Instead, just synchronously merge the returned state into the instance.
newState =
newState === null || newState === undefined
? partialState
: Object.assign({}, newState, partialState);
}
if (
oldProps === newProps &&
oldState === newState &&
@@ -679,14 +871,44 @@ export default function(
);
if (shouldUpdate) {
if (typeof instance.componentWillUpdate === 'function') {
startPhaseTimer(workInProgress, 'componentWillUpdate');
instance.componentWillUpdate(newProps, newState, newContext);
stopPhaseTimer();
if (
typeof instance.UNSAFE_componentWillUpdate === 'function' ||
typeof instance.componentWillUpdate === 'function'
) {
if (typeof instance.componentWillUpdate === 'function') {
if (__DEV__) {
if (warnAboutDeprecatedLifecycles) {
const componentName =
getComponentName(workInProgress) || 'Component';
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;
}
}
}
// Simulate an async bailout/interruption by invoking lifecycle twice.
if (debugRenderPhaseSideEffects) {
startPhaseTimer(workInProgress, 'componentWillUpdate');
instance.componentWillUpdate(newProps, newState, newContext);
stopPhaseTimer();
} else {
startPhaseTimer(workInProgress, 'componentWillUpdate');
instance.UNSAFE_componentWillUpdate(newProps, newState, newContext);
stopPhaseTimer();
// Simulate an async bailout/interruption by invoking lifecycle twice.
if (debugRenderPhaseSideEffects) {
instance.UNSAFE_componentWillUpdate(newProps, newState, newContext);
}
}
}
if (typeof instance.componentDidUpdate === 'function') {
@@ -721,6 +943,7 @@ export default function(
return {
adoptClassInstance,
callGetDerivedStateFromProps,
constructClassInstance,
mountClassInstance,
// resumeMountClassInstance,
@@ -571,16 +571,16 @@ describe('ReactIncremental', () => {
ops.push('constructor: ' + props.prop);
constructorCount++;
}
componentWillMount() {
UNSAFE_componentWillMount() {
ops.push('componentWillMount: ' + this.props.prop);
}
componentWillReceiveProps() {
UNSAFE_componentWillReceiveProps() {
ops.push('componentWillReceiveProps: ' + this.props.prop);
}
componentDidMount() {
ops.push('componentDidMount: ' + this.props.prop);
}
componentWillUpdate() {
UNSAFE_componentWillUpdate() {
ops.push('componentWillUpdate: ' + this.props.prop);
}
componentDidUpdate() {
@@ -1232,13 +1232,13 @@ describe('ReactIncremental', () => {
class LifeCycle extends React.Component {
state = {x: this.props.x};
componentWillReceiveProps(nextProps) {
UNSAFE_componentWillReceiveProps(nextProps) {
ops.push(
'componentWillReceiveProps:' + this.state.x + '-' + nextProps.x,
);
this.setState({x: nextProps.x});
}
componentWillMount() {
UNSAFE_componentWillMount() {
ops.push('componentWillMount:' + this.state.x + '-' + this.props.x);
}
componentDidMount() {
@@ -1291,7 +1291,7 @@ describe('ReactIncremental', () => {
super(props);
this.state = {x: this.props.x + '(ctor)'};
}
componentWillMount() {
UNSAFE_componentWillMount() {
ops.push('componentWillMount:' + this.state.x);
this.setState({x: this.props.x + '(willMount)'});
}
@@ -1334,13 +1334,13 @@ describe('ReactIncremental', () => {
let ops = [];
class LifeCycle extends React.Component {
componentWillMount() {
UNSAFE_componentWillMount() {
ops.push('componentWillMount:' + this.props.x);
}
componentDidMount() {
ops.push('componentDidMount:' + this.props.x);
}
componentWillReceiveProps(nextProps) {
UNSAFE_componentWillReceiveProps(nextProps) {
ops.push(
'componentWillReceiveProps:' + this.props.x + '-' + nextProps.x,
);
@@ -1349,7 +1349,7 @@ describe('ReactIncremental', () => {
ops.push('shouldComponentUpdate:' + this.props.x + '-' + nextProps.x);
return true;
}
componentWillUpdate(nextProps) {
UNSAFE_componentWillUpdate(nextProps) {
ops.push('componentWillUpdate:' + this.props.x + '-' + nextProps.x);
}
componentDidUpdate(prevProps) {
@@ -1417,6 +1417,44 @@ describe('ReactIncremental', () => {
]);
});
it('does not call static getDerivedStateFromProps for state-only updates', () => {
let ops = [];
let instance;
class LifeCycle extends React.Component {
state = {};
static getDerivedStateFromProps(props, prevState) {
ops.push('getDerivedStateFromProps');
return {foo: 'foo'};
}
changeState() {
this.setState({foo: 'bar'});
}
componentWillUpdate() {
ops.push('componentWillUpdate');
}
render() {
ops.push('render');
instance = this;
return null;
}
}
ReactNoop.render(<LifeCycle />);
ReactNoop.flush();
expect(ops).toEqual(['getDerivedStateFromProps', 'render']);
expect(instance.state).toEqual({foo: 'foo'});
ops = [];
instance.changeState();
ReactNoop.flush();
expect(ops).toEqual(['componentWillUpdate', 'render']);
expect(instance.state).toEqual({foo: 'bar'});
});
xit('does not call componentWillReceiveProps for state-only updates', () => {
let ops = [];
@@ -1429,21 +1467,21 @@ describe('ReactIncremental', () => {
x: this.state.x + 1,
});
}
componentWillMount() {
UNSAFE_componentWillMount() {
instances.push(this);
ops.push('componentWillMount:' + this.state.x);
}
componentDidMount() {
ops.push('componentDidMount:' + this.state.x);
}
componentWillReceiveProps(nextProps) {
UNSAFE_componentWillReceiveProps(nextProps) {
ops.push('componentWillReceiveProps');
}
shouldComponentUpdate(nextProps, nextState) {
ops.push('shouldComponentUpdate:' + this.state.x + '-' + nextState.x);
return true;
}
componentWillUpdate(nextProps, nextState) {
UNSAFE_componentWillUpdate(nextProps, nextState) {
ops.push('componentWillUpdate:' + this.state.x + '-' + nextState.x);
}
componentDidUpdate(prevProps, prevState) {
@@ -1460,7 +1498,7 @@ describe('ReactIncremental', () => {
// output unless it fully completed.
class Wrap extends React.Component {
state = {y: 0};
componentWillMount() {
UNSAFE_componentWillMount() {
instances.push(this);
}
tick() {
@@ -1570,13 +1608,13 @@ describe('ReactIncremental', () => {
let ops = [];
class LifeCycle extends React.Component {
componentWillMount() {
UNSAFE_componentWillMount() {
ops.push('componentWillMount');
}
componentDidMount() {
ops.push('componentDidMount');
}
componentWillReceiveProps(nextProps) {
UNSAFE_componentWillReceiveProps(nextProps) {
ops.push('componentWillReceiveProps');
}
shouldComponentUpdate(nextProps) {
@@ -1584,7 +1622,7 @@ describe('ReactIncremental', () => {
// Bail
return this.props.x !== nextProps.x;
}
componentWillUpdate(nextProps) {
UNSAFE_componentWillUpdate(nextProps) {
ops.push('componentWillUpdate');
}
componentDidUpdate(prevProps) {
@@ -2282,7 +2320,7 @@ describe('ReactIncremental', () => {
this.setState({setStateInCDU: false});
}
}
componentWillReceiveProps(nextProps) {
UNSAFE_componentWillReceiveProps(nextProps) {
ops.push('componentWillReceiveProps');
this.setState({setStateInCDU: true});
}
@@ -26,7 +26,7 @@ describe('ReactIncrementalErrorLogging', () => {
spyOnProd(console, 'error');
class ErrorThrowingComponent extends React.Component {
componentWillMount() {
UNSAFE_componentWillMount() {
const error = new Error('componentWillMount error');
// Note: it's `true` on the Error prototype our test environment.
// That lets us avoid asserting on warnings for each expected error.
@@ -237,10 +237,10 @@ describe('ReactDebugFiberPerf', () => {
it('does not treat setState from cWM or cWRP as cascading', () => {
class NotCascading extends React.Component {
componentWillMount() {
UNSAFE_componentWillMount() {
this.setState({});
}
componentWillReceiveProps() {
UNSAFE_componentWillReceiveProps() {
this.setState({});
}
render() {
@@ -276,10 +276,10 @@ describe('ReactDebugFiberPerf', () => {
getChildContext() {
return {foo: 42};
}
componentWillMount() {}
UNSAFE_componentWillMount() {}
componentDidMount() {}
componentWillReceiveProps() {}
componentWillUpdate() {}
UNSAFE_componentWillReceiveProps() {}
UNSAFE_componentWillUpdate() {}
componentDidUpdate() {}
componentWillUnmount() {}
render() {
@@ -31,7 +31,7 @@ describe('ReactIncrementalReflection', () => {
// reaching into the updater.
return this.updater.isMounted(this);
}
componentWillMount() {
UNSAFE_componentWillMount() {
instances.push(this);
ops.push('componentWillMount', this._isMounted());
}
@@ -75,7 +75,7 @@ describe('ReactIncrementalReflection', () => {
_isMounted() {
return this.updater.isMounted(this);
}
componentWillMount() {
UNSAFE_componentWillMount() {
instances.push(this);
}
componentWillUnmount() {
@@ -128,14 +128,14 @@ describe('ReactIncrementalReflection', () => {
let classInstance = null;
class Component extends React.Component {
componentWillMount() {
UNSAFE_componentWillMount() {
classInstance = this;
ops.push('componentWillMount', ReactNoop.findInstance(this));
}
componentDidMount() {
ops.push('componentDidMount', ReactNoop.findInstance(this));
}
componentWillUpdate() {
UNSAFE_componentWillUpdate() {
ops.push('componentWillUpdate', ReactNoop.findInstance(this));
}
componentDidUpdate() {
@@ -331,7 +331,7 @@ describe('ReactIncrementalScheduling', () => {
it('updates do not schedule a new callback if already inside a callback', () => {
class Foo extends React.Component {
state = {foo: 'foo'};
componentWillReceiveProps() {
UNSAFE_componentWillReceiveProps() {
ReactNoop.yield(
'has callback before setState: ' + ReactNoop.hasScheduledCallback(),
);
@@ -296,7 +296,7 @@ describe('ReactIncrementalUpdates', () => {
class Foo extends React.Component {
state = {};
componentWillReceiveProps() {
UNSAFE_componentWillReceiveProps() {
ops.push('componentWillReceiveProps');
this.setState({b: 'b'});
}
+186 -12
View File
@@ -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() {
@@ -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(
@@ -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;
@@ -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);
}
@@ -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) {
@@ -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;
@@ -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);
}
}
@@ -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 {
@@ -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 />);
});
});
@@ -16,6 +16,8 @@ let React;
describe('ReactShallowRenderer', () => {
beforeEach(() => {
jest.resetModules();
createRenderer = require('react-test-renderer/shallow').createRenderer;
PropTypes = require('prop-types');
React = require('react');
@@ -26,11 +28,13 @@ describe('ReactShallowRenderer', () => {
const logger = message => () => logs.push(message) || true;
class SomeComponent extends React.Component {
componentWillMount = logger('componentWillMount');
state = {};
static getDerivedStateFromProps = logger('getDerivedStateFromProps');
UNSAFE_componentWillMount = logger('componentWillMount');
componentDidMount = logger('componentDidMount');
componentWillReceiveProps = logger('componentWillReceiveProps');
UNSAFE_componentWillReceiveProps = logger('componentWillReceiveProps');
shouldComponentUpdate = logger('shouldComponentUpdate');
componentWillUpdate = logger('componentWillUpdate');
UNSAFE_componentWillUpdate = logger('componentWillUpdate');
componentDidUpdate = logger('componentDidUpdate');
componentWillUnmount = logger('componentWillUnmount');
render() {
@@ -39,11 +43,16 @@ describe('ReactShallowRenderer', () => {
}
const shallowRenderer = createRenderer();
shallowRenderer.render(<SomeComponent foo={1} />);
expect(() => shallowRenderer.render(<SomeComponent foo={1} />)).toWarnDev(
'Warning: SomeComponent: Defines both componentWillReceiveProps() and static ' +
'getDerivedStateFromProps() methods. ' +
'We recommend using only getDerivedStateFromProps().',
);
// 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(['componentWillMount']);
expect(logs).toEqual(['getDerivedStateFromProps', 'componentWillMount']);
logs.splice(0);
@@ -59,6 +68,7 @@ describe('ReactShallowRenderer', () => {
// The previous shallow renderer did not trigger cDU for props changes.
expect(logs).toEqual([
'componentWillReceiveProps',
'getDerivedStateFromProps',
'shouldComponentUpdate',
'componentWillUpdate',
]);
@@ -416,6 +426,7 @@ describe('ReactShallowRenderer', () => {
const componentDidUpdateParams = [];
const componentWillReceivePropsParams = [];
const componentWillUpdateParams = [];
const getDerivedStateFromPropsParams = [];
const setStateParams = [];
const shouldComponentUpdateParams = [];
@@ -437,14 +448,18 @@ describe('ReactShallowRenderer', () => {
componentDidUpdate(...args) {
componentDidUpdateParams.push(...args);
}
componentWillReceiveProps(...args) {
static getDerivedStateFromProps(...args) {
getDerivedStateFromPropsParams.push(args);
return null;
}
UNSAFE_componentWillReceiveProps(...args) {
componentWillReceivePropsParams.push(...args);
this.setState((...innerArgs) => {
setStateParams.push(...innerArgs);
return updatedState;
});
}
componentWillUpdate(...args) {
UNSAFE_componentWillUpdate(...args) {
componentWillUpdateParams.push(...args);
}
shouldComponentUpdate(...args) {
@@ -458,11 +473,21 @@ describe('ReactShallowRenderer', () => {
const shallowRenderer = createRenderer();
// No lifecycle hooks should be invoked on initial render
shallowRenderer.render(
React.createElement(SimpleComponent, initialProp),
initialContext,
// 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().',
);
expect(getDerivedStateFromPropsParams).toEqual([
[initialProp, initialState],
]);
expect(componentDidUpdateParams).toEqual([]);
expect(componentWillReceivePropsParams).toEqual([]);
expect(componentWillUpdateParams).toEqual([]);
@@ -479,6 +504,10 @@ describe('ReactShallowRenderer', () => {
updatedContext,
]);
expect(setStateParams).toEqual([initialState, initialProp]);
expect(getDerivedStateFromPropsParams).toEqual([
[initialProp, initialState],
[updatedProp, initialState],
]);
expect(shouldComponentUpdateParams).toEqual([
updatedProp,
updatedState,
@@ -523,9 +552,34 @@ describe('ReactShallowRenderer', () => {
expect(result.props.className).toEqual('clicked');
});
it('can initialize state via static getDerivedStateFromProps', () => {
class SimpleComponent extends React.Component {
state = {
count: 1,
};
static getDerivedStateFromProps(props, prevState) {
return {
count: prevState.count + props.incrementBy,
other: 'foobar',
};
}
render() {
return (
<div>{`count:${this.state.count}, other:${this.state.other}`}</div>
);
}
}
const shallowRenderer = createRenderer();
const result = shallowRenderer.render(<SimpleComponent incrementBy={2} />);
expect(result).toEqual(<div>count:3, other:foobar</div>);
});
it('can setState in componentWillMount when shallow rendering', () => {
class SimpleComponent extends React.Component {
componentWillMount() {
UNSAFE_componentWillMount() {
this.setState({groovy: 'doovy'});
}
@@ -545,7 +599,7 @@ describe('ReactShallowRenderer', () => {
separator: '-',
};
componentWillMount() {
UNSAFE_componentWillMount() {
this.setState({groovy: 'doovy'});
this.setState({doovy: 'groovy'});
}
@@ -568,7 +622,7 @@ describe('ReactShallowRenderer', () => {
separator: '-',
};
componentWillMount() {
UNSAFE_componentWillMount() {
this.setState(state => ({groovy: 'doovy'}));
this.setState(state => ({doovy: state.groovy}));
}
@@ -589,7 +643,7 @@ describe('ReactShallowRenderer', () => {
class SimpleComponent extends React.Component {
state = {count: 0};
componentWillReceiveProps(nextProps) {
UNSAFE_componentWillReceiveProps(nextProps) {
if (nextProps.updateState) {
this.setState({count: 1});
}
@@ -610,6 +664,40 @@ describe('ReactShallowRenderer', () => {
expect(result.props.children).toEqual(1);
});
it('can update state with static getDerivedStateFromProps when shallow rendering', () => {
class SimpleComponent extends React.Component {
state = {count: 1};
static getDerivedStateFromProps(nextProps, prevState) {
if (nextProps.updateState) {
return {count: nextProps.incrementBy + prevState.count};
}
return null;
}
render() {
return <div>{this.state.count}</div>;
}
}
const shallowRenderer = createRenderer();
let result = shallowRenderer.render(
<SimpleComponent updateState={false} incrementBy={0} />,
);
expect(result.props.children).toEqual(1);
result = shallowRenderer.render(
<SimpleComponent updateState={true} incrementBy={2} />,
);
expect(result.props.children).toEqual(3);
result = shallowRenderer.render(
<SimpleComponent updateState={false} incrementBy={2} />,
);
expect(result.props.children).toEqual(3);
});
it('can setState with an updater function', () => {
let instance;
@@ -870,7 +958,7 @@ describe('ReactShallowRenderer', () => {
};
}
componentWillMount() {
UNSAFE_componentWillMount() {
this.setState(
{hasUpdatedState: true},
() => (stateSuccessfullyUpdated = this.state.hasUpdatedState),
@@ -899,7 +987,7 @@ describe('ReactShallowRenderer', () => {
};
}
componentWillMount() {
UNSAFE_componentWillMount() {
this.setState({foo: 'bar'}, () => mockFn());
this.setState({foo: 'foobar'}, () => mockFn());
}
@@ -984,4 +1072,66 @@ describe('ReactShallowRenderer', () => {
expect(shallowRenderer.getMountedInstance().state).toBeNull();
});
it('should warn if both componentWillReceiveProps and static getDerivedStateFromProps exist', () => {
class ComponentWithWarnings extends React.Component {
state = {};
static getDerivedStateFromProps(props, prevState) {
return null;
}
UNSAFE_componentWillReceiveProps(nextProps) {}
render() {
return null;
}
}
const shallowRenderer = createRenderer();
expect(() => shallowRenderer.render(<ComponentWithWarnings />)).toWarnDev(
'ComponentWithWarnings: Defines both componentWillReceiveProps() and static ' +
'getDerivedStateFromProps() methods. We recommend using ' +
'only getDerivedStateFromProps().',
);
// Should not log duplicate warning
shallowRenderer.render(<ComponentWithWarnings />);
});
it('should warn if getDerivedStateFromProps returns undefined', () => {
class Component extends React.Component {
state = {};
static getDerivedStateFromProps() {}
render() {
return null;
}
}
const shallowRenderer = createRenderer();
expect(() => shallowRenderer.render(<Component />)).toWarnDev(
'Component.getDerivedStateFromProps(): A valid state object (or null) must ' +
'be returned. You have returned undefined.',
);
// De-duped
shallowRenderer.render(<Component />);
});
it('should warn if state not initialized before getDerivedStateFromProps', () => {
class Component extends React.Component {
static getDerivedStateFromProps() {
return null;
}
render() {
return null;
}
}
const shallowRenderer = createRenderer();
expect(() => shallowRenderer.render(<Component />)).toWarnDev(
'Component: Did not properly initialize state during construction. ' +
'Expected state to be an object, but it was undefined.',
);
// De-duped
shallowRenderer.render(<Component />);
});
});
@@ -38,16 +38,16 @@ describe('ReactAsyncClassComponent', () => {
componentDidUpdate() {
log.push('componentDidUpdate');
}
componentWillMount() {
UNSAFE_componentWillMount() {
log.push('componentWillMount');
}
componentWillReceiveProps() {
UNSAFE_componentWillReceiveProps() {
log.push('componentWillReceiveProps');
}
componentWillUnmount() {
log.push('componentWillUnmount');
}
componentWillUpdate() {
UNSAFE_componentWillUpdate() {
log.push('componentWillUpdate');
}
shouldComponentUpdate() {
@@ -98,6 +98,73 @@ describe 'ReactCoffeeScriptClass', ->
test React.createElement(Foo), 'SPAN', 'bar'
undefined
it 'sets initial state with value returned by static getDerivedStateFromProps', ->
class Foo extends React.Component
constructor: (props) ->
super props
@state = foo: null
render: ->
div
className: "#{@state.foo} #{@state.bar}"
Foo.getDerivedStateFromProps = (nextProps, prevState) ->
{
foo: nextProps.foo
bar: 'bar'
}
test React.createElement(Foo, foo: 'foo'), 'DIV', 'foo bar'
undefined
it 'warns if state not initialized before static getDerivedStateFromProps', ->
class Foo extends React.Component
render: ->
div
className: "#{@state.foo} #{@state.bar}"
Foo.getDerivedStateFromProps = (nextProps, prevState) ->
{
foo: nextProps.foo
bar: 'bar'
}
expect(->
ReactDOM.render(React.createElement(Foo, foo: 'foo'), container)
).toWarnDev 'Foo: Did not properly initialize state during construction. Expected state to be an object, but it was undefined.'
undefined
it 'updates initial state with values returned by static getDerivedStateFromProps', ->
class Foo extends React.Component
constructor: (props, context) ->
super props, context
@state =
foo: 'foo'
bar: 'bar'
render: ->
div
className: "#{@state.foo} #{@state.bar}"
Foo.getDerivedStateFromProps = (nextProps, prevState) ->
{
foo: "not-#{prevState.foo}"
}
test React.createElement(Foo), 'DIV', 'not-foo bar'
undefined
it 'renders updated state with values returned by static getDerivedStateFromProps', ->
class Foo extends React.Component
constructor: (props, context) ->
super props, context
@state =
value: 'initial'
render: ->
div
className: @state.value
Foo.getDerivedStateFromProps = (nextProps, prevState) ->
if nextProps.update
return {
value: 'updated'
}
return null
test React.createElement(Foo, update: false), 'DIV', 'initial'
test React.createElement(Foo, update: true), 'DIV', 'updated'
undefined
it 'renders based on context in the constructor', ->
class Foo extends React.Component
@contextTypes:
@@ -136,7 +203,7 @@ describe 'ReactCoffeeScriptClass', ->
constructor: (props) ->
@state = bar: props.initialValue
componentWillMount: ->
UNSAFE_componentWillMount: ->
@setState bar: 'bar'
render: ->
@@ -232,20 +299,20 @@ describe 'ReactCoffeeScriptClass', ->
constructor: ->
@state = {}
componentWillMount: ->
UNSAFE_componentWillMount: ->
lifeCycles.push 'will-mount'
componentDidMount: ->
lifeCycles.push 'did-mount'
componentWillReceiveProps: (nextProps) ->
UNSAFE_componentWillReceiveProps: (nextProps) ->
lifeCycles.push 'receive-props', nextProps
shouldComponentUpdate: (nextProps, nextState) ->
lifeCycles.push 'should-update', nextProps, nextState
true
componentWillUpdate: (nextProps, nextState) ->
UNSAFE_componentWillUpdate: (nextProps, nextState) ->
lifeCycles.push 'will-update', nextProps, nextState
componentDidUpdate: (prevProps, prevState) ->
@@ -361,6 +428,23 @@ describe 'ReactCoffeeScriptClass', ->
)
undefined
it 'should warn when misspelling UNSAFE_componentWillReceiveProps', ->
class NamedComponent extends React.Component
UNSAFE_componentWillRecieveProps: ->
false
render: ->
span
className: 'foo'
expect(->
test React.createElement(NamedComponent), 'SPAN', 'foo'
).toWarnDev(
'Warning: NamedComponent has a method called UNSAFE_componentWillRecieveProps().
Did you mean UNSAFE_componentWillReceiveProps()?'
)
undefined
it 'should throw AND warn when trying to access classic APIs', ->
instance =
test Inner(name: 'foo'), 'DIV', 'foo'
@@ -89,7 +89,7 @@ describe('ReactContextValidator', () => {
};
class Component extends React.Component {
componentWillReceiveProps(nextProps, nextContext) {
UNSAFE_componentWillReceiveProps(nextProps, nextContext) {
actualComponentWillReceiveProps = nextContext;
return true;
}
@@ -99,7 +99,7 @@ describe('ReactContextValidator', () => {
return true;
}
componentWillUpdate(nextProps, nextState, nextContext) {
UNSAFE_componentWillUpdate(nextProps, nextState, nextContext) {
actualComponentWillUpdate = nextContext;
}
@@ -109,6 +109,79 @@ describe('ReactES6Class', () => {
test(<Foo />, 'SPAN', 'bar');
});
it('sets initial state with value returned by static getDerivedStateFromProps', () => {
class Foo extends React.Component {
state = {};
static getDerivedStateFromProps(nextProps, prevState) {
return {
foo: nextProps.foo,
bar: 'bar',
};
}
render() {
return <div className={`${this.state.foo} ${this.state.bar}`} />;
}
}
test(<Foo foo="foo" />, 'DIV', 'foo bar');
});
it('warns if state not initialized before static getDerivedStateFromProps', () => {
class Foo extends React.Component {
static getDerivedStateFromProps(nextProps, prevState) {
return {
foo: nextProps.foo,
bar: 'bar',
};
}
render() {
return <div className={`${this.state.foo} ${this.state.bar}`} />;
}
}
expect(() => ReactDOM.render(<Foo foo="foo" />, container)).toWarnDev(
'Foo: Did not properly initialize state during construction. ' +
'Expected state to be an object, but it was undefined.',
);
});
it('updates initial state with values returned by static getDerivedStateFromProps', () => {
class Foo extends React.Component {
state = {
foo: 'foo',
bar: 'bar',
};
static getDerivedStateFromProps(nextProps, prevState) {
return {
foo: `not-${prevState.foo}`,
};
}
render() {
return <div className={`${this.state.foo} ${this.state.bar}`} />;
}
}
test(<Foo />, 'DIV', 'not-foo bar');
});
it('renders updated state with values returned by static getDerivedStateFromProps', () => {
class Foo extends React.Component {
state = {
value: 'initial',
};
static getDerivedStateFromProps(nextProps, prevState) {
if (nextProps.update) {
return {
value: 'updated',
};
}
return null;
}
render() {
return <div className={this.state.value} />;
}
}
test(<Foo update={false} />, 'DIV', 'initial');
test(<Foo update={true} />, 'DIV', 'updated');
});
it('renders based on context in the constructor', () => {
class Foo extends React.Component {
constructor(props, context) {
@@ -147,7 +220,7 @@ describe('ReactES6Class', () => {
super(props);
this.state = {bar: props.initialValue};
}
componentWillMount() {
UNSAFE_componentWillMount() {
this.setState({bar: 'bar'});
}
render() {
@@ -257,20 +330,20 @@ describe('ReactES6Class', () => {
super();
this.state = {};
}
componentWillMount() {
UNSAFE_componentWillMount() {
lifeCycles.push('will-mount');
}
componentDidMount() {
lifeCycles.push('did-mount');
}
componentWillReceiveProps(nextProps) {
UNSAFE_componentWillReceiveProps(nextProps) {
lifeCycles.push('receive-props', nextProps);
}
shouldComponentUpdate(nextProps, nextState) {
lifeCycles.push('should-update', nextProps, nextState);
return true;
}
componentWillUpdate(nextProps, nextState) {
UNSAFE_componentWillUpdate(nextProps, nextState) {
lifeCycles.push('will-update', nextProps, nextState);
}
componentDidUpdate(prevProps, prevState) {
@@ -379,6 +452,23 @@ describe('ReactES6Class', () => {
);
});
it('should warn when misspelling UNSAFE_componentWillReceiveProps', () => {
class NamedComponent extends React.Component {
UNSAFE_componentWillRecieveProps() {
return false;
}
render() {
return <span className="foo" />;
}
}
expect(() => test(<NamedComponent />, 'SPAN', 'foo')).toWarnDev(
'Warning: ' +
'NamedComponent has a method called UNSAFE_componentWillRecieveProps(). ' +
'Did you mean UNSAFE_componentWillReceiveProps()?',
);
});
it('should throw AND warn when trying to access classic APIs', () => {
const instance = test(<Inner name="foo" />, 'DIV', 'foo');
expect(() =>
@@ -117,7 +117,7 @@ class RenderOnce extends React.Component {
state = {
bar: this.props.initialValue,
};
componentWillMount() {
UNSAFE_componentWillMount() {
this.setState({bar: 'bar'});
}
render() {
@@ -206,20 +206,20 @@ let lifeCycles = [];
class NormalLifeCycles extends React.Component {
props: any;
state = {};
componentWillMount() {
UNSAFE_componentWillMount() {
lifeCycles.push('will-mount');
}
componentDidMount() {
lifeCycles.push('did-mount');
}
componentWillReceiveProps(nextProps) {
UNSAFE_componentWillReceiveProps(nextProps) {
lifeCycles.push('receive-props', nextProps);
}
shouldComponentUpdate(nextProps, nextState) {
lifeCycles.push('should-update', nextProps, nextState);
return true;
}
componentWillUpdate(nextProps, nextState) {
UNSAFE_componentWillUpdate(nextProps, nextState) {
lifeCycles.push('will-update', nextProps, nextState);
}
componentDidUpdate(prevProps, prevState) {
@@ -273,6 +273,16 @@ class MisspelledComponent2 extends React.Component {
}
}
// it should warn when misspelling UNSAFE_componentWillReceiveProps
class MisspelledComponent3 extends React.Component {
UNSAFE_componentWillRecieveProps() {
return false;
}
render() {
return React.createElement('span', {className: 'foo'});
}
}
// it supports this.context passed via getChildContext
class ReadContext extends React.Component {
static contextTypes = {bar: PropTypes.string};
@@ -344,6 +354,84 @@ describe('ReactTypeScriptClass', function() {
test(React.createElement(StateBasedOnProps), 'SPAN', 'bar');
});
it('sets initial state with value returned by static getDerivedStateFromProps', function() {
class Foo extends React.Component {
state = {
foo: null,
bar: null,
};
static getDerivedStateFromProps(nextProps, prevState) {
return {
foo: nextProps.foo,
bar: 'bar',
};
}
render() {
return React.createElement('div', {className: `${this.state.foo} ${this.state.bar}`});
}
}
test(React.createElement(Foo, {foo: "foo"}), 'DIV', 'foo bar');
});
it('warns if state not initialized before static getDerivedStateFromProps', function() {
class Foo extends React.Component {
static getDerivedStateFromProps(nextProps, prevState) {
return {
foo: nextProps.foo,
bar: 'bar',
};
}
render() {
return React.createElement('div', {className: `${this.state.foo} ${this.state.bar}`});
}
}
expect(function() {
ReactDOM.render(React.createElement(Foo, {foo: "foo"}), container);
}).toWarnDev(
'Foo: Did not properly initialize state during construction. ' +
'Expected state to be an object, but it was undefined.'
);
});
it('updates initial state with values returned by static getDerivedStateFromProps', function() {
class Foo extends React.Component {
state = {
foo: 'foo',
bar: 'bar',
};
static getDerivedStateFromProps(nextProps, prevState) {
return {
foo: `not-${prevState.foo}`,
};
}
render() {
return React.createElement('div', {className: `${this.state.foo} ${this.state.bar}`});
}
}
test(React.createElement(Foo), 'DIV', 'not-foo bar');
});
it('renders updated state with values returned by static getDerivedStateFromProps', function() {
class Foo extends React.Component {
state = {
value: 'initial',
};
static getDerivedStateFromProps(nextProps, prevState) {
if (nextProps.update) {
return {
value: 'updated',
};
}
return null;
}
render() {
return React.createElement('div', {className: this.state.value});
}
}
test(React.createElement(Foo, {update:false}), 'DIV', 'initial');
test(React.createElement(Foo, {update:true}), 'DIV', 'updated');
});
it('renders based on context in the constructor', function() {
test(React.createElement(ProvideChildContextTypes), 'SPAN', 'foo');
});
@@ -483,6 +571,16 @@ describe('ReactTypeScriptClass', function() {
);
});
it('should warn when misspelling UNSAFE_componentWillReceiveProps', function() {
expect(() =>
test(React.createElement(MisspelledComponent3), 'SPAN', 'foo')
).toWarnDev(
'Warning: ' +
'MisspelledComponent3 has a method called UNSAFE_componentWillRecieveProps(). ' +
'Did you mean UNSAFE_componentWillReceiveProps()?'
);
});
it('should throw AND warn when trying to access classic APIs', function() {
const instance = test(
React.createElement(Inner, {name: 'foo'}),
@@ -0,0 +1,126 @@
/**
* 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
*/
'use strict';
let React;
let ReactDOM;
let ReactFeatureFlags;
let createReactClass;
describe('create-react-class-integration', () => {
beforeEach(() => {
ReactFeatureFlags = require('shared/ReactFeatureFlags');
ReactFeatureFlags.warnAboutDeprecatedLifecycles = true;
React = require('react');
ReactDOM = require('react-dom');
createReactClass = require('create-react-class/factory')(
React.Component,
React.isValidElement,
new React.Component().updater,
);
});
// TODO (RFC #6) Merge this back into createReactClassIntegration-test once
// the 'warnAboutDeprecatedLifecycles' feature flag has been removed.
it('isMounted works', () => {
const ops = [];
let instance;
const Component = createReactClass({
displayName: 'MyComponent',
mixins: [
{
componentWillMount() {
this.log('mixin.componentWillMount');
},
componentDidMount() {
this.log('mixin.componentDidMount');
},
componentWillUpdate() {
this.log('mixin.componentWillUpdate');
},
componentDidUpdate() {
this.log('mixin.componentDidUpdate');
},
componentWillUnmount() {
this.log('mixin.componentWillUnmount');
},
},
],
log(name) {
ops.push(`${name}: ${this.isMounted()}`);
},
getInitialState() {
this.log('getInitialState');
return {};
},
componentWillMount() {
this.log('componentWillMount');
},
componentDidMount() {
this.log('componentDidMount');
},
componentWillUpdate() {
this.log('componentWillUpdate');
},
componentDidUpdate() {
this.log('componentDidUpdate');
},
componentWillUnmount() {
this.log('componentWillUnmount');
},
render() {
instance = this;
this.log('render');
return <div />;
},
});
const container = document.createElement('div');
// TODO (RFC #6) The below lifecycle warnings are unavoidable for now,
// Until create-react-class recognizes the UNSAFE_* methods.
// (If we try to use them before them, it will error because
// we are defining the same method twice.)
// Update the above component to use the new UNSAFE_* methods
// (and remove the expected warnings) once create-react-class has been updated.
expect(() => ReactDOM.render(<Component />, container)).toWarnDev([
'Warning: MyComponent: isMounted is deprecated. Instead, make sure to ' +
'clean up subscriptions and pending requests in componentWillUnmount ' +
'to prevent memory leaks.',
'Warning: MyComponent: componentWillMount() is deprecated and will be ' +
'removed in the next major version.',
]);
expect(() => ReactDOM.render(<Component />, container)).toWarnDev(
'Warning: MyComponent: componentWillUpdate() is deprecated and will be ' +
'removed in the next major version.',
);
ReactDOM.unmountComponentAtNode(container);
instance.log('after unmount');
expect(ops).toEqual([
'getInitialState: false',
'mixin.componentWillMount: false',
'componentWillMount: false',
'render: false',
'mixin.componentDidMount: true',
'componentDidMount: true',
'mixin.componentWillUpdate: true',
'componentWillUpdate: true',
'render: true',
'mixin.componentDidUpdate: true',
'componentDidUpdate: true',
'mixin.componentWillUnmount: true',
'componentWillUnmount: true',
'after unmount: false',
]);
});
});
@@ -17,6 +17,7 @@ let createReactClass;
describe('create-react-class-integration', () => {
beforeEach(() => {
jest.resetModules();
PropTypes = require('prop-types');
React = require('react');
ReactDOM = require('react-dom');
@@ -151,6 +152,23 @@ describe('create-react-class-integration', () => {
);
});
// TODO (RFC #6) Reenable after create-react-class updated.
xit('should warn when misspelling UNSAFE_componentWillReceiveProps', () => {
expect(() =>
createReactClass({
UNSAFE_componentWillRecieveProps: function() {
return false;
},
render: function() {
return <div />;
},
}),
).toWarnDev(
'Warning: A component has a method called UNSAFE_componentWillRecieveProps(). ' +
'Did you mean UNSAFE_componentWillReceiveProps()?',
);
});
it('should throw if a reserved property is in statics', () => {
expect(function() {
createReactClass({
@@ -175,7 +193,6 @@ describe('create-react-class-integration', () => {
});
// TODO: Consider actually moving these to statics or drop this unit test.
xit('should warn when using deprecated non-static spec keys', () => {
expect(() =>
createReactClass({
@@ -251,6 +268,23 @@ describe('create-react-class-integration', () => {
expect(instance.state.occupation).toEqual('clown');
});
it('should work with getDerivedStateFromProps() return values', () => {
const Component = createReactClass({
getInitialState() {
return {};
},
render: function() {
return <span />;
},
});
Component.getDerivedStateFromProps = () => {
return {occupation: 'clown'};
};
let instance = <Component />;
instance = ReactTestUtils.renderIntoDocument(instance);
expect(instance.state.occupation).toEqual('clown');
});
it('renders based on context getInitialState', () => {
const Foo = createReactClass({
contextTypes: {
@@ -346,85 +380,73 @@ describe('create-react-class-integration', () => {
expect(ops).toEqual(['Render: 0', 'Render: 1', 'Callback: 1']);
});
it('isMounted works', () => {
const ops = [];
let instance;
it('getDerivedStateFromProps updates state when props change', () => {
const Component = createReactClass({
displayName: 'MyComponent',
mixins: [
{
componentWillMount() {
this.log('mixin.componentWillMount');
},
componentDidMount() {
this.log('mixin.componentDidMount');
},
componentWillUpdate() {
this.log('mixin.componentWillUpdate');
},
componentDidUpdate() {
this.log('mixin.componentDidUpdate');
},
componentWillUnmount() {
this.log('mixin.componentWillUnmount');
},
},
],
log(name) {
ops.push(`${name}: ${this.isMounted()}`);
},
getInitialState() {
this.log('getInitialState');
return {};
},
componentWillMount() {
this.log('componentWillMount');
},
componentDidMount() {
this.log('componentDidMount');
},
componentWillUpdate() {
this.log('componentWillUpdate');
},
componentDidUpdate() {
this.log('componentDidUpdate');
},
componentWillUnmount() {
this.log('componentWillUnmount');
return {
count: 1,
};
},
render() {
instance = this;
this.log('render');
return <div />;
return <div>count:{this.state.count}</div>;
},
});
Component.getDerivedStateFromProps = (nextProps, prevState) => ({
count: prevState.count + nextProps.incrementBy,
});
const container = document.createElement('div');
expect(() => ReactDOM.render(<Component />, container)).toWarnDev(
'Warning: MyComponent: isMounted is deprecated. Instead, make sure to ' +
'clean up subscriptions and pending requests in componentWillUnmount ' +
'to prevent memory leaks.',
const instance = ReactDOM.render(
<div>
<Component incrementBy={0} />
</div>,
container,
);
expect(instance.textContent).toEqual('count:1');
ReactDOM.render(
<div>
<Component incrementBy={2} />
</div>,
container,
);
expect(instance.textContent).toEqual('count:3');
});
ReactDOM.render(<Component />, container);
ReactDOM.unmountComponentAtNode(container);
instance.log('after unmount');
expect(ops).toEqual([
'getInitialState: false',
'mixin.componentWillMount: false',
'componentWillMount: false',
'render: false',
'mixin.componentDidMount: true',
'componentDidMount: true',
'mixin.componentWillUpdate: true',
'componentWillUpdate: true',
'render: true',
'mixin.componentDidUpdate: true',
'componentDidUpdate: true',
'mixin.componentWillUnmount: true',
'componentWillUnmount: true',
'after unmount: false',
]);
it('should support the new static getDerivedStateFromProps method', () => {
let instance;
const Component = createReactClass({
statics: {
getDerivedStateFromProps: function() {
return {foo: 'bar'};
},
},
getInitialState() {
return {};
},
render: function() {
instance = this;
return null;
},
});
ReactDOM.render(<Component />, document.createElement('div'));
expect(instance.state.foo).toBe('bar');
});
it('should warn if state is not properly initialized before getDerivedStateFromProps', () => {
const Component = createReactClass({
statics: {
getDerivedStateFromProps: function() {
return null;
},
},
render: function() {
return null;
},
});
expect(() =>
ReactDOM.render(<Component />, document.createElement('div')),
).toWarnDev('Did not properly initialize state during construction.');
});
});
+3
View File
@@ -24,6 +24,9 @@ export const enablePersistentReconciler = false;
// Helps identify side effects in begin-phase lifecycle hooks and setState reducers:
export const debugRenderPhaseSideEffects = false;
// Warn about deprecated, async-unsafe lifecycles; relates to RFC #6:
export const warnAboutDeprecatedLifecycles = false;
// Only used in www builds.
export function addUserTimingListener() {
invariant(false, 'Not implemented.');
@@ -16,6 +16,7 @@ export const debugRenderPhaseSideEffects = false;
export const enableAsyncSubtreeAPI = true;
export const enableCreateRoot = false;
export const enableUserTimingAPI = __DEV__;
export const warnAboutDeprecatedLifecycles = false;
// React Native CS uses persistent reconciler.
export const enableMutatingReconciler = false;
@@ -13,7 +13,10 @@ import typeof * as FeatureFlagsType from 'shared/ReactFeatureFlags';
import typeof * as FeatureFlagsShimType from './ReactFeatureFlags.native';
// Re-export dynamic flags from the fbsource version.
export const {debugRenderPhaseSideEffects} = require('ReactFeatureFlags');
export const {
debugRenderPhaseSideEffects,
warnAboutDeprecatedLifecycles,
} = require('ReactFeatureFlags');
// The rest of the flags are static for better dead code elimination.
export const enableAsyncSubtreeAPI = true;
@@ -11,7 +11,10 @@ import typeof * as FeatureFlagsType from 'shared/ReactFeatureFlags';
import typeof * as FeatureFlagsShimType from './ReactFeatureFlags.www';
// Re-export dynamic flags from the www version.
export const {debugRenderPhaseSideEffects} = require('ReactFeatureFlags');
export const {
debugRenderPhaseSideEffects,
warnAboutDeprecatedLifecycles,
} = require('ReactFeatureFlags');
// The rest of the flags are static for better dead code elimination.
export const enableAsyncSubtreeAPI = true;