Skip to content

Commit ec51957

Browse files
koba04ljharb
authored andcommitted
[Fix] shallow: prevent rerenders with PureComponents
Fixes #1784.
1 parent 6db69c3 commit ec51957

File tree

5 files changed

+179
-0
lines changed

5 files changed

+179
-0
lines changed

packages/enzyme-test-suite/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
"enzyme": "^3.3.0",
3434
"enzyme-adapter-utils": "^1.5.0",
3535
"jsdom": "^6.5.1",
36+
"lodash.isequal": "^4.5.0",
3637
"mocha-wrap": "^2.1.2",
3738
"object.assign": "^4.1.0",
3839
"object-inspect": "^1.6.0",

packages/enzyme-test-suite/test/ReactWrapper-spec.jsx

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import PropTypes from 'prop-types';
44
import { expect } from 'chai';
55
import sinon from 'sinon';
66
import wrap from 'mocha-wrap';
7+
import isEqual from 'lodash.isequal';
78
import {
89
mount,
910
render,
@@ -27,6 +28,7 @@ import {
2728
createRef,
2829
Fragment,
2930
forwardRef,
31+
PureComponent,
3032
} from './_helpers/react-compat';
3133
import {
3234
describeWithDOM,
@@ -5128,6 +5130,78 @@ describeWithDOM('mount', () => {
51285130
});
51295131
});
51305132

5133+
describeIf(is('>= 15.3'), 'PureComponent', () => {
5134+
it('does not update when state and props did not change', () => {
5135+
class Foo extends PureComponent {
5136+
constructor(props) {
5137+
super(props);
5138+
this.state = {
5139+
foo: 'init',
5140+
};
5141+
}
5142+
5143+
componentDidUpdate() {}
5144+
5145+
render() {
5146+
return (
5147+
<div>
5148+
{this.state.foo}
5149+
</div>
5150+
);
5151+
}
5152+
}
5153+
const spy = sinon.spy(Foo.prototype, 'componentDidUpdate');
5154+
const wrapper = mount(<Foo id={1} />);
5155+
wrapper.setState({ foo: 'update' });
5156+
expect(spy).to.have.property('callCount', 1);
5157+
wrapper.setState({ foo: 'update' });
5158+
expect(spy).to.have.property('callCount', 1);
5159+
5160+
wrapper.setProps({ id: 2 });
5161+
expect(spy).to.have.property('callCount', 2);
5162+
wrapper.setProps({ id: 2 });
5163+
expect(spy).to.have.property('callCount', 2);
5164+
});
5165+
});
5166+
5167+
describe('Own PureComponent implementation', () => {
5168+
it('does not update when state and props did not change', () => {
5169+
class Foo extends React.Component {
5170+
constructor(props) {
5171+
super(props);
5172+
this.state = {
5173+
foo: 'init',
5174+
};
5175+
}
5176+
5177+
shouldComponentUpdate(nextProps, nextState) {
5178+
return !isEqual(this.props, nextProps) || !isEqual(this.state, nextState);
5179+
}
5180+
5181+
componentDidUpdate() {}
5182+
5183+
render() {
5184+
return (
5185+
<div>
5186+
{this.state.foo}
5187+
</div>
5188+
);
5189+
}
5190+
}
5191+
const spy = sinon.spy(Foo.prototype, 'componentDidUpdate');
5192+
const wrapper = mount(<Foo id={1} />);
5193+
wrapper.setState({ foo: 'update' });
5194+
expect(spy).to.have.property('callCount', 1);
5195+
wrapper.setState({ foo: 'update' });
5196+
expect(spy).to.have.property('callCount', 1);
5197+
5198+
wrapper.setProps({ id: 2 });
5199+
expect(spy).to.have.property('callCount', 2);
5200+
wrapper.setProps({ id: 2 });
5201+
expect(spy).to.have.property('callCount', 2);
5202+
});
5203+
});
5204+
51315205
describeIf(is('>= 16.3'), 'support getSnapshotBeforeUpdate', () => {
51325206
it('calls getSnapshotBeforeUpdate and pass snapshot to componentDidUpdate', () => {
51335207
const spy = sinon.spy();

packages/enzyme-test-suite/test/ShallowWrapper-spec.jsx

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import PropTypes from 'prop-types';
33
import { expect } from 'chai';
44
import sinon from 'sinon';
55
import wrap from 'mocha-wrap';
6+
import isEqual from 'lodash.isequal';
67
import {
78
shallow,
89
render,
@@ -27,6 +28,7 @@ import {
2728
createRef,
2829
Fragment,
2930
forwardRef,
31+
PureComponent,
3032
} from './_helpers/react-compat';
3133
import {
3234
describeIf,
@@ -5499,6 +5501,78 @@ describe('shallow', () => {
54995501
});
55005502
});
55015503

5504+
describeIf(is('>= 15.3'), 'PureComponent', () => {
5505+
it('does not update when state and props did not change', () => {
5506+
class Foo extends PureComponent {
5507+
constructor(props) {
5508+
super(props);
5509+
this.state = {
5510+
foo: 'init',
5511+
};
5512+
}
5513+
5514+
componentDidUpdate() {}
5515+
5516+
render() {
5517+
return (
5518+
<div>
5519+
{this.state.foo}
5520+
</div>
5521+
);
5522+
}
5523+
}
5524+
const spy = sinon.spy(Foo.prototype, 'componentDidUpdate');
5525+
const wrapper = shallow(<Foo id={1} />);
5526+
wrapper.setState({ foo: 'update' });
5527+
expect(spy).to.have.property('callCount', 1);
5528+
wrapper.setState({ foo: 'update' });
5529+
expect(spy).to.have.property('callCount', 1);
5530+
5531+
wrapper.setProps({ id: 2 });
5532+
expect(spy).to.have.property('callCount', 2);
5533+
wrapper.setProps({ id: 2 });
5534+
expect(spy).to.have.property('callCount', 2);
5535+
});
5536+
});
5537+
5538+
describe('Own PureComponent implementation', () => {
5539+
it('does not update when state and props did not change', () => {
5540+
class Foo extends React.Component {
5541+
constructor(props) {
5542+
super(props);
5543+
this.state = {
5544+
foo: 'init',
5545+
};
5546+
}
5547+
5548+
shouldComponentUpdate(nextProps, nextState) {
5549+
return !isEqual(this.props, nextProps) || !isEqual(this.state, nextState);
5550+
}
5551+
5552+
componentDidUpdate() {}
5553+
5554+
render() {
5555+
return (
5556+
<div>
5557+
{this.state.foo}
5558+
</div>
5559+
);
5560+
}
5561+
}
5562+
const spy = sinon.spy(Foo.prototype, 'componentDidUpdate');
5563+
const wrapper = mount(<Foo id={1} />);
5564+
wrapper.setState({ foo: 'update' });
5565+
expect(spy).to.have.property('callCount', 1);
5566+
wrapper.setState({ foo: 'update' });
5567+
expect(spy).to.have.property('callCount', 1);
5568+
5569+
wrapper.setProps({ id: 2 });
5570+
expect(spy).to.have.property('callCount', 2);
5571+
wrapper.setProps({ id: 2 });
5572+
expect(spy).to.have.property('callCount', 2);
5573+
});
5574+
});
5575+
55025576
describeIf(is('>= 16.3'), 'support getSnapshotBeforeUpdate', () => {
55035577
it('calls getSnapshotBeforeUpdate and pass snapshot to componentDidUpdate', () => {
55045578
const spy = sinon.spy();

packages/enzyme-test-suite/test/_helpers/react-compat.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ let Fragment;
1616
let StrictMode;
1717
let AsyncMode;
1818
let Profiler;
19+
let PureComponent;
1920

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

41+
if (is('>=15.3')) {
42+
({ PureComponent } = require('react'));
43+
} else {
44+
PureComponent = null;
45+
}
46+
4047
if (is('^16.2.0-0')) {
4148
({ Fragment } = require('react'));
4249
} else {
@@ -78,4 +85,5 @@ export {
7885
StrictMode,
7986
AsyncMode,
8087
Profiler,
88+
PureComponent,
8189
};

packages/enzyme/src/ShallowWrapper.js

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,14 @@ function privateSetNodes(wrapper, nodes) {
158158
privateSet(wrapper, 'length', wrapper[NODES].length);
159159
}
160160

161+
function pureComponentShouldComponentUpdate(prevProps, props, prevState, state) {
162+
return !isEqual(prevProps, props) || !isEqual(prevState, state);
163+
}
164+
165+
function isPureComponent(instance) {
166+
return instance && instance.isPureReactComponent;
167+
}
168+
161169
/**
162170
* @class ShallowWrapper
163171
*/
@@ -342,6 +350,13 @@ class ShallowWrapper {
342350
&& typeof instance.shouldComponentUpdate === 'function'
343351
) {
344352
spy = spyMethod(instance, 'shouldComponentUpdate');
353+
} else if (isPureComponent(instance)) {
354+
shouldRender = pureComponentShouldComponentUpdate(
355+
prevProps,
356+
props,
357+
state,
358+
instance.state,
359+
);
345360
}
346361
if (props) this[UNRENDERED] = cloneElement(adapter, this[UNRENDERED], props);
347362
this[RENDERER].render(this[UNRENDERED], nextContext);
@@ -472,6 +487,13 @@ class ShallowWrapper {
472487
&& typeof instance.shouldComponentUpdate === 'function'
473488
) {
474489
spy = spyMethod(instance, 'shouldComponentUpdate');
490+
} else if (isPureComponent(instance)) {
491+
shouldRender = pureComponentShouldComponentUpdate(
492+
prevProps,
493+
instance.props,
494+
prevState,
495+
statePayload,
496+
);
475497
}
476498
// We don't pass the setState callback here
477499
// to guarantee to call the callback after finishing the render

0 commit comments

Comments
 (0)