diff --git a/packages/enzyme-test-suite/package.json b/packages/enzyme-test-suite/package.json
index fc118bd50..597e25d66 100644
--- a/packages/enzyme-test-suite/package.json
+++ b/packages/enzyme-test-suite/package.json
@@ -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",
diff --git a/packages/enzyme-test-suite/test/ReactWrapper-spec.jsx b/packages/enzyme-test-suite/test/ReactWrapper-spec.jsx
index 6c30a4df8..abbf38dc8 100644
--- a/packages/enzyme-test-suite/test/ReactWrapper-spec.jsx
+++ b/packages/enzyme-test-suite/test/ReactWrapper-spec.jsx
@@ -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,
@@ -27,6 +28,7 @@ import {
createRef,
Fragment,
forwardRef,
+ PureComponent,
} from './_helpers/react-compat';
import {
describeWithDOM,
@@ -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 (
+
+ {this.state.foo}
+
+ );
+ }
+ }
+ const spy = sinon.spy(Foo.prototype, 'componentDidUpdate');
+ const wrapper = mount();
+ 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 (
+
+ {this.state.foo}
+
+ );
+ }
+ }
+ const spy = sinon.spy(Foo.prototype, 'componentDidUpdate');
+ const wrapper = mount();
+ 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();
diff --git a/packages/enzyme-test-suite/test/ShallowWrapper-spec.jsx b/packages/enzyme-test-suite/test/ShallowWrapper-spec.jsx
index 92a0b9e47..c561b2bbb 100644
--- a/packages/enzyme-test-suite/test/ShallowWrapper-spec.jsx
+++ b/packages/enzyme-test-suite/test/ShallowWrapper-spec.jsx
@@ -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,
@@ -27,6 +28,7 @@ import {
createRef,
Fragment,
forwardRef,
+ PureComponent,
} from './_helpers/react-compat';
import {
describeIf,
@@ -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 (
+
+ {this.state.foo}
+
+ );
+ }
+ }
+ const spy = sinon.spy(Foo.prototype, 'componentDidUpdate');
+ const wrapper = shallow();
+ 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 (
+
+ {this.state.foo}
+
+ );
+ }
+ }
+ const spy = sinon.spy(Foo.prototype, 'componentDidUpdate');
+ const wrapper = mount();
+ 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();
diff --git a/packages/enzyme-test-suite/test/_helpers/react-compat.js b/packages/enzyme-test-suite/test/_helpers/react-compat.js
index 3b36c3847..7fb9d72df 100644
--- a/packages/enzyme-test-suite/test/_helpers/react-compat.js
+++ b/packages/enzyme-test-suite/test/_helpers/react-compat.js
@@ -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
@@ -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 {
@@ -78,4 +85,5 @@ export {
StrictMode,
AsyncMode,
Profiler,
+ PureComponent,
};
diff --git a/packages/enzyme/src/ShallowWrapper.js b/packages/enzyme/src/ShallowWrapper.js
index 8160100b9..204aabd7c 100644
--- a/packages/enzyme/src/ShallowWrapper.js
+++ b/packages/enzyme/src/ShallowWrapper.js
@@ -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
*/
@@ -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);
@@ -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