Skip to content

Commit

Permalink
React lifecycles compat (#12105)
Browse files Browse the repository at this point in the history
* Suppress unsafe/deprecation warnings for polyfilled components.
* Don't invoke deprecated lifecycles if static gDSFP exists.
* Applied recent changes to server rendering also
  • Loading branch information
bvaughn authored Jan 29, 2018
1 parent 4a38d6d commit 8e3532a
Show file tree
Hide file tree
Showing 3 changed files with 205 additions and 44 deletions.
45 changes: 37 additions & 8 deletions src/ReactShallowRenderer.js
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,12 @@ class ReactShallowRenderer {

if (typeof this._instance.componentWillMount === 'function') {
if (__DEV__) {
if (warnAboutDeprecatedLifecycles) {
// Don't warn about react-lifecycles-compat polyfilled components
if (
warnAboutDeprecatedLifecycles &&
this._instance.componentWillMount.__suppressDeprecationWarning !==
true
) {
const componentName = getName(element.type, this._instance);
if (!didWarnAboutLegacyWillMount[componentName]) {
warning(
Expand All @@ -198,8 +203,15 @@ class ReactShallowRenderer {
}
}
}
this._instance.componentWillMount();
} else {

// In order to support react-lifecycles-compat polyfilled components,
// Unsafe lifecycles should not be invoked for any component with the new gDSFP.
if (typeof element.type.getDerivedStateFromProps !== 'function') {
this._instance.componentWillMount();
}
} else if (typeof element.type.getDerivedStateFromProps !== 'function') {
// In order to support react-lifecycles-compat polyfilled components,
// Unsafe lifecycles should not be invoked for any component with the new gDSFP.
this._instance.UNSAFE_componentWillMount();
}

Expand Down Expand Up @@ -242,10 +254,17 @@ class ReactShallowRenderer {
}
}
}
this._instance.componentWillReceiveProps(props, context);
// In order to support react-lifecycles-compat polyfilled components,
// Unsafe lifecycles should not be invoked for any component with the new gDSFP.
if (typeof element.type.getDerivedStateFromProps !== 'function') {
this._instance.componentWillReceiveProps(props, context);
}
} else if (
typeof this._instance.UNSAFE_componentWillReceiveProps === 'function'
typeof this._instance.UNSAFE_componentWillReceiveProps === 'function' &&
typeof element.type.getDerivedStateFromProps !== 'function'
) {
// In order to support react-lifecycles-compat polyfilled components,
// Unsafe lifecycles should not be invoked for any component with the new gDSFP.
this._instance.UNSAFE_componentWillReceiveProps(props, context);
}

Expand Down Expand Up @@ -292,10 +311,17 @@ class ReactShallowRenderer {
}
}

this._instance.componentWillUpdate(props, state, context);
// In order to support react-lifecycles-compat polyfilled components,
// Unsafe lifecycles should not be invoked for any component with the new gDSFP.
if (typeof type.getDerivedStateFromProps !== 'function') {
this._instance.componentWillUpdate(props, state, context);
}
} else if (
typeof this._instance.UNSAFE_componentWillUpdate === 'function'
typeof this._instance.UNSAFE_componentWillUpdate === 'function' &&
typeof type.getDerivedStateFromProps !== 'function'
) {
// In order to support react-lifecycles-compat polyfilled components,
// Unsafe lifecycles should not be invoked for any component with the new gDSFP.
this._instance.UNSAFE_componentWillUpdate(props, state, context);
}
}
Expand All @@ -316,8 +342,11 @@ class ReactShallowRenderer {

if (typeof type.getDerivedStateFromProps === 'function') {
if (__DEV__) {
// Don't warn about react-lifecycles-compat polyfilled components
if (
typeof this._instance.componentWillReceiveProps === 'function' ||
(typeof this._instance.componentWillReceiveProps === 'function' &&
this._instance.componentWillReceiveProps
.__suppressDeprecationWarning !== true) ||
typeof this._instance.UNSAFE_componentWillReceiveProps === 'function'
) {
const componentName = getName(type, this._instance);
Expand Down
31 changes: 31 additions & 0 deletions src/__tests__/ReactShallowRenderer-test.internal.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ describe('ReactShallowRenderer', () => {
React = require('react');
});

afterEach(() => {
jest.resetModules();
});

// TODO (RFC #6) Merge this back into ReactShallowRenderer-test once
// the 'warnAboutDeprecatedLifecycles' feature flag has been removed.
it('should warn if deprecated lifecycles exist', () => {
Expand Down Expand Up @@ -50,4 +54,31 @@ describe('ReactShallowRenderer', () => {
// Verify no duplicate warnings
shallowRenderer.render(<ComponentWithWarnings />);
});

describe('react-lifecycles-compat', () => {
// TODO Replace this with react-lifecycles-compat once it's been published
function polyfill(Component) {
Component.prototype.componentWillMount = function() {};
Component.prototype.componentWillMount.__suppressDeprecationWarning = true;
Component.prototype.componentWillReceiveProps = function() {};
Component.prototype.componentWillReceiveProps.__suppressDeprecationWarning = true;
}

it('should not warn about deprecated cWM/cWRP for polyfilled components', () => {
class PolyfilledComponent extends React.Component {
state = {};
static getDerivedStateFromProps() {
return null;
}
render() {
return null;
}
}

polyfill(PolyfilledComponent);

const shallowRenderer = createRenderer();
shallowRenderer.render(<PolyfilledComponent />);
});
});
});
173 changes: 137 additions & 36 deletions src/__tests__/ReactShallowRenderer-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,11 @@ describe('ReactShallowRenderer', () => {
React = require('react');
});

it('should call all of the lifecycle hooks', () => {
it('should call all of the legacy lifecycle hooks', () => {
const logs = [];
const logger = message => () => logs.push(message) || true;

class SomeComponent extends React.Component {
state = {};
static getDerivedStateFromProps = logger('getDerivedStateFromProps');
UNSAFE_componentWillMount = logger('componentWillMount');
componentDidMount = logger('componentDidMount');
UNSAFE_componentWillReceiveProps = logger('componentWillReceiveProps');
Expand All @@ -43,16 +41,11 @@ describe('ReactShallowRenderer', () => {
}

const shallowRenderer = createRenderer();

expect(() => shallowRenderer.render(<SomeComponent foo={1} />)).toWarnDev(
'Warning: SomeComponent: Defines both componentWillReceiveProps() and static ' +
'getDerivedStateFromProps() methods. ' +
'We recommend using only getDerivedStateFromProps().',
);
shallowRenderer.render(<SomeComponent foo={1} />);

// Calling cDU might lead to problems with host component references.
// Since our components aren't really mounted, refs won't be available.
expect(logs).toEqual(['getDerivedStateFromProps', 'componentWillMount']);
expect(logs).toEqual(['componentWillMount']);

logs.splice(0);

Expand All @@ -68,12 +61,75 @@ describe('ReactShallowRenderer', () => {
// The previous shallow renderer did not trigger cDU for props changes.
expect(logs).toEqual([
'componentWillReceiveProps',
'getDerivedStateFromProps',
'shouldComponentUpdate',
'componentWillUpdate',
]);
});

it('should call all of the new lifecycle hooks', () => {
const logs = [];
const logger = message => () => logs.push(message) || true;

class SomeComponent extends React.Component {
state = {};
static getDerivedStateFromProps = logger('getDerivedStateFromProps');
componentDidMount = logger('componentDidMount');
shouldComponentUpdate = logger('shouldComponentUpdate');
componentDidUpdate = logger('componentDidUpdate');
componentWillUnmount = logger('componentWillUnmount');
render() {
return <div />;
}
}

const shallowRenderer = createRenderer();
shallowRenderer.render(<SomeComponent foo={1} />);

// Calling cDU might lead to problems with host component references.
// Since our components aren't really mounted, refs won't be available.
expect(logs).toEqual(['getDerivedStateFromProps']);

logs.splice(0);

const instance = shallowRenderer.getMountedInstance();
instance.setState({});

expect(logs).toEqual(['shouldComponentUpdate']);

logs.splice(0);

shallowRenderer.render(<SomeComponent foo={2} />);

// The previous shallow renderer did not trigger cDU for props changes.
expect(logs).toEqual(['getDerivedStateFromProps', 'shouldComponentUpdate']);
});

it('should not invoke deprecated lifecycles (cWM/cWRP/cWU) if new static gDSFP is present', () => {
class Component extends React.Component {
state = {};
static getDerivedStateFromProps() {
return null;
}
componentWillMount() {
throw Error('unexpected');
}
componentWillReceiveProps() {
throw Error('unexpected');
}
componentWillUpdate() {
throw Error('unexpected');
}
render() {
return null;
}
}

const shallowRenderer = createRenderer();
expect(() => shallowRenderer.render(<Component foo={2} />)).toWarnDev(
'Defines both componentWillReceiveProps() and static getDerivedStateFromProps()',
);
});

it('should only render 1 level deep', () => {
function Parent() {
return (
Expand Down Expand Up @@ -422,11 +478,10 @@ describe('ReactShallowRenderer', () => {
expect(result).toEqual(<div />);
});

it('passes expected params to component lifecycle methods', () => {
it('passes expected params to legacy component lifecycle methods', () => {
const componentDidUpdateParams = [];
const componentWillReceivePropsParams = [];
const componentWillUpdateParams = [];
const getDerivedStateFromPropsParams = [];
const setStateParams = [];
const shouldComponentUpdateParams = [];

Expand All @@ -448,10 +503,6 @@ describe('ReactShallowRenderer', () => {
componentDidUpdate(...args) {
componentDidUpdateParams.push(...args);
}
static getDerivedStateFromProps(...args) {
getDerivedStateFromPropsParams.push(args);
return null;
}
UNSAFE_componentWillReceiveProps(...args) {
componentWillReceivePropsParams.push(...args);
this.setState((...innerArgs) => {
Expand All @@ -472,22 +523,10 @@ describe('ReactShallowRenderer', () => {
}

const shallowRenderer = createRenderer();

// The only lifecycle hook that should be invoked on initial render
// Is the static getDerivedStateFromProps() methods
expect(() =>
shallowRenderer.render(
React.createElement(SimpleComponent, initialProp),
initialContext,
),
).toWarnDev(
'SimpleComponent: Defines both componentWillReceiveProps() and static ' +
'getDerivedStateFromProps() methods. We recommend using ' +
'only getDerivedStateFromProps().',
shallowRenderer.render(
React.createElement(SimpleComponent, initialProp),
initialContext,
);
expect(getDerivedStateFromPropsParams).toEqual([
[initialProp, initialState],
]);
expect(componentDidUpdateParams).toEqual([]);
expect(componentWillReceivePropsParams).toEqual([]);
expect(componentWillUpdateParams).toEqual([]);
Expand All @@ -504,10 +543,6 @@ describe('ReactShallowRenderer', () => {
updatedContext,
]);
expect(setStateParams).toEqual([initialState, initialProp]);
expect(getDerivedStateFromPropsParams).toEqual([
[initialProp, initialState],
[updatedProp, initialState],
]);
expect(shouldComponentUpdateParams).toEqual([
updatedProp,
updatedState,
Expand All @@ -521,6 +556,72 @@ describe('ReactShallowRenderer', () => {
expect(componentDidUpdateParams).toEqual([]);
});

it('passes expected params to new component lifecycle methods', () => {
const componentDidUpdateParams = [];
const getDerivedStateFromPropsParams = [];
const shouldComponentUpdateParams = [];

const initialProp = {prop: 'init prop'};
const initialState = {state: 'init state'};
const initialContext = {context: 'init context'};
const updatedProp = {prop: 'updated prop'};
const updatedContext = {context: 'updated context'};

class SimpleComponent extends React.Component {
constructor(props, context) {
super(props, context);
this.state = initialState;
}
static contextTypes = {
context: PropTypes.string,
};
componentDidUpdate(...args) {
componentDidUpdateParams.push(...args);
}
static getDerivedStateFromProps(...args) {
getDerivedStateFromPropsParams.push(args);
return null;
}
shouldComponentUpdate(...args) {
shouldComponentUpdateParams.push(...args);
return true;
}
render() {
return null;
}
}

const shallowRenderer = createRenderer();

// The only lifecycle hook that should be invoked on initial render
// Is the static getDerivedStateFromProps() methods
shallowRenderer.render(
React.createElement(SimpleComponent, initialProp),
initialContext,
);
expect(getDerivedStateFromPropsParams).toEqual([
[initialProp, initialState],
]);
expect(componentDidUpdateParams).toEqual([]);
expect(shouldComponentUpdateParams).toEqual([]);

// Lifecycle hooks should be invoked with the correct prev/next params on update.
shallowRenderer.render(
React.createElement(SimpleComponent, updatedProp),
updatedContext,
);
expect(getDerivedStateFromPropsParams).toEqual([
[initialProp, initialState],
[updatedProp, initialState],
]);
expect(shouldComponentUpdateParams).toEqual([
updatedProp,
initialState,
updatedContext,
]);
expect(componentDidUpdateParams).toEqual([]);
});

it('can shallowly render components with ref as function', () => {
class SimpleComponent extends React.Component {
state = {clicked: false};
Expand Down

0 comments on commit 8e3532a

Please sign in to comment.