Skip to content

Commit

Permalink
[Fix] shallow: prevent rerenders with PureComponents
Browse files Browse the repository at this point in the history
Fixes #1784.
  • Loading branch information
koba04 authored and ljharb committed Aug 22, 2018
1 parent 6db69c3 commit ec51957
Show file tree
Hide file tree
Showing 5 changed files with 179 additions and 0 deletions.
1 change: 1 addition & 0 deletions packages/enzyme-test-suite/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
"enzyme": "^3.3.0",
"enzyme-adapter-utils": "^1.5.0",
"jsdom": "^6.5.1",
"lodash.isequal": "^4.5.0",
"mocha-wrap": "^2.1.2",
"object.assign": "^4.1.0",
"object-inspect": "^1.6.0",
Expand Down
74 changes: 74 additions & 0 deletions packages/enzyme-test-suite/test/ReactWrapper-spec.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import PropTypes from 'prop-types';
import { expect } from 'chai';
import sinon from 'sinon';
import wrap from 'mocha-wrap';
import isEqual from 'lodash.isequal';
import {
mount,
render,
Expand All @@ -27,6 +28,7 @@ import {
createRef,
Fragment,
forwardRef,
PureComponent,
} from './_helpers/react-compat';
import {
describeWithDOM,
Expand Down Expand Up @@ -5128,6 +5130,78 @@ describeWithDOM('mount', () => {
});
});

describeIf(is('>= 15.3'), 'PureComponent', () => {
it('does not update when state and props did not change', () => {
class Foo extends PureComponent {
constructor(props) {
super(props);
this.state = {
foo: 'init',
};
}

componentDidUpdate() {}

render() {
return (
<div>
{this.state.foo}
</div>
);
}
}
const spy = sinon.spy(Foo.prototype, 'componentDidUpdate');
const wrapper = mount(<Foo id={1} />);
wrapper.setState({ foo: 'update' });
expect(spy).to.have.property('callCount', 1);
wrapper.setState({ foo: 'update' });
expect(spy).to.have.property('callCount', 1);

wrapper.setProps({ id: 2 });
expect(spy).to.have.property('callCount', 2);
wrapper.setProps({ id: 2 });
expect(spy).to.have.property('callCount', 2);
});
});

describe('Own PureComponent implementation', () => {
it('does not update when state and props did not change', () => {
class Foo extends React.Component {
constructor(props) {
super(props);
this.state = {
foo: 'init',
};
}

shouldComponentUpdate(nextProps, nextState) {
return !isEqual(this.props, nextProps) || !isEqual(this.state, nextState);
}

componentDidUpdate() {}

render() {
return (
<div>
{this.state.foo}
</div>
);
}
}
const spy = sinon.spy(Foo.prototype, 'componentDidUpdate');
const wrapper = mount(<Foo id={1} />);
wrapper.setState({ foo: 'update' });
expect(spy).to.have.property('callCount', 1);
wrapper.setState({ foo: 'update' });
expect(spy).to.have.property('callCount', 1);

wrapper.setProps({ id: 2 });
expect(spy).to.have.property('callCount', 2);
wrapper.setProps({ id: 2 });
expect(spy).to.have.property('callCount', 2);
});
});

describeIf(is('>= 16.3'), 'support getSnapshotBeforeUpdate', () => {
it('calls getSnapshotBeforeUpdate and pass snapshot to componentDidUpdate', () => {
const spy = sinon.spy();
Expand Down
74 changes: 74 additions & 0 deletions packages/enzyme-test-suite/test/ShallowWrapper-spec.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import PropTypes from 'prop-types';
import { expect } from 'chai';
import sinon from 'sinon';
import wrap from 'mocha-wrap';
import isEqual from 'lodash.isequal';
import {
shallow,
render,
Expand All @@ -27,6 +28,7 @@ import {
createRef,
Fragment,
forwardRef,
PureComponent,
} from './_helpers/react-compat';
import {
describeIf,
Expand Down Expand Up @@ -5499,6 +5501,78 @@ describe('shallow', () => {
});
});

describeIf(is('>= 15.3'), 'PureComponent', () => {
it('does not update when state and props did not change', () => {
class Foo extends PureComponent {
constructor(props) {
super(props);
this.state = {
foo: 'init',
};
}

componentDidUpdate() {}

render() {
return (
<div>
{this.state.foo}
</div>
);
}
}
const spy = sinon.spy(Foo.prototype, 'componentDidUpdate');
const wrapper = shallow(<Foo id={1} />);
wrapper.setState({ foo: 'update' });
expect(spy).to.have.property('callCount', 1);
wrapper.setState({ foo: 'update' });
expect(spy).to.have.property('callCount', 1);

wrapper.setProps({ id: 2 });
expect(spy).to.have.property('callCount', 2);
wrapper.setProps({ id: 2 });
expect(spy).to.have.property('callCount', 2);
});
});

describe('Own PureComponent implementation', () => {
it('does not update when state and props did not change', () => {
class Foo extends React.Component {
constructor(props) {
super(props);
this.state = {
foo: 'init',
};
}

shouldComponentUpdate(nextProps, nextState) {
return !isEqual(this.props, nextProps) || !isEqual(this.state, nextState);
}

componentDidUpdate() {}

render() {
return (
<div>
{this.state.foo}
</div>
);
}
}
const spy = sinon.spy(Foo.prototype, 'componentDidUpdate');
const wrapper = mount(<Foo id={1} />);
wrapper.setState({ foo: 'update' });
expect(spy).to.have.property('callCount', 1);
wrapper.setState({ foo: 'update' });
expect(spy).to.have.property('callCount', 1);

wrapper.setProps({ id: 2 });
expect(spy).to.have.property('callCount', 2);
wrapper.setProps({ id: 2 });
expect(spy).to.have.property('callCount', 2);
});
});

describeIf(is('>= 16.3'), 'support getSnapshotBeforeUpdate', () => {
it('calls getSnapshotBeforeUpdate and pass snapshot to componentDidUpdate', () => {
const spy = sinon.spy();
Expand Down
8 changes: 8 additions & 0 deletions packages/enzyme-test-suite/test/_helpers/react-compat.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ let Fragment;
let StrictMode;
let AsyncMode;
let Profiler;
let PureComponent;

if (is('>=15.5 || ^16.0.0-alpha || ^16.3.0-alpha')) {
// eslint-disable-next-line import/no-extraneous-dependencies
Expand All @@ -37,6 +38,12 @@ if (is('^16.0.0-0 || ^16.3.0-0')) {
createPortal = null;
}

if (is('>=15.3')) {
({ PureComponent } = require('react'));
} else {
PureComponent = null;
}

if (is('^16.2.0-0')) {
({ Fragment } = require('react'));
} else {
Expand Down Expand Up @@ -78,4 +85,5 @@ export {
StrictMode,
AsyncMode,
Profiler,
PureComponent,
};
22 changes: 22 additions & 0 deletions packages/enzyme/src/ShallowWrapper.js
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,14 @@ function privateSetNodes(wrapper, nodes) {
privateSet(wrapper, 'length', wrapper[NODES].length);
}

function pureComponentShouldComponentUpdate(prevProps, props, prevState, state) {
return !isEqual(prevProps, props) || !isEqual(prevState, state);
}

function isPureComponent(instance) {
return instance && instance.isPureReactComponent;
}

/**
* @class ShallowWrapper
*/
Expand Down Expand Up @@ -342,6 +350,13 @@ class ShallowWrapper {
&& typeof instance.shouldComponentUpdate === 'function'
) {
spy = spyMethod(instance, 'shouldComponentUpdate');
} else if (isPureComponent(instance)) {
shouldRender = pureComponentShouldComponentUpdate(
prevProps,
props,
state,
instance.state,
);
}
if (props) this[UNRENDERED] = cloneElement(adapter, this[UNRENDERED], props);
this[RENDERER].render(this[UNRENDERED], nextContext);
Expand Down Expand Up @@ -472,6 +487,13 @@ class ShallowWrapper {
&& typeof instance.shouldComponentUpdate === 'function'
) {
spy = spyMethod(instance, 'shouldComponentUpdate');
} else if (isPureComponent(instance)) {
shouldRender = pureComponentShouldComponentUpdate(
prevProps,
instance.props,
prevState,
statePayload,
);
}
// We don't pass the setState callback here
// to guarantee to call the callback after finishing the render
Expand Down

0 comments on commit ec51957

Please sign in to comment.