diff --git a/packages/enzyme-adapter-react-16.1/src/ReactSixteenOneAdapter.js b/packages/enzyme-adapter-react-16.1/src/ReactSixteenOneAdapter.js
index 54d0d605b..4da8391e7 100644
--- a/packages/enzyme-adapter-react-16.1/src/ReactSixteenOneAdapter.js
+++ b/packages/enzyme-adapter-react-16.1/src/ReactSixteenOneAdapter.js
@@ -220,6 +220,9 @@ class ReactSixteenOneAdapter extends EnzymeAdapter {
componentDidUpdate: {
onSetState: true,
},
+ setState: {
+ skipsComponentDidUpdateOnNullish: true,
+ },
},
};
}
diff --git a/packages/enzyme-adapter-react-16.2/src/ReactSixteenTwoAdapter.js b/packages/enzyme-adapter-react-16.2/src/ReactSixteenTwoAdapter.js
index f5e52fed2..15845bddb 100644
--- a/packages/enzyme-adapter-react-16.2/src/ReactSixteenTwoAdapter.js
+++ b/packages/enzyme-adapter-react-16.2/src/ReactSixteenTwoAdapter.js
@@ -222,6 +222,9 @@ class ReactSixteenTwoAdapter extends EnzymeAdapter {
componentDidUpdate: {
onSetState: true,
},
+ setState: {
+ skipsComponentDidUpdateOnNullish: true,
+ },
},
};
}
diff --git a/packages/enzyme-adapter-react-16.3/src/ReactSixteenThreeAdapter.js b/packages/enzyme-adapter-react-16.3/src/ReactSixteenThreeAdapter.js
index fc0d700e7..a0cdc69ef 100644
--- a/packages/enzyme-adapter-react-16.3/src/ReactSixteenThreeAdapter.js
+++ b/packages/enzyme-adapter-react-16.3/src/ReactSixteenThreeAdapter.js
@@ -241,6 +241,9 @@ class ReactSixteenThreeAdapter extends EnzymeAdapter {
onSetState: true,
},
getSnapshotBeforeUpdate: true,
+ setState: {
+ skipsComponentDidUpdateOnNullish: true,
+ },
},
};
}
diff --git a/packages/enzyme-adapter-react-16/src/ReactSixteenAdapter.js b/packages/enzyme-adapter-react-16/src/ReactSixteenAdapter.js
index c20e9e7aa..43d0f7d07 100644
--- a/packages/enzyme-adapter-react-16/src/ReactSixteenAdapter.js
+++ b/packages/enzyme-adapter-react-16/src/ReactSixteenAdapter.js
@@ -245,6 +245,9 @@ class ReactSixteenAdapter extends EnzymeAdapter {
onSetState: true,
},
getSnapshotBeforeUpdate: true,
+ setState: {
+ skipsComponentDidUpdateOnNullish: true,
+ },
},
};
}
diff --git a/packages/enzyme-test-suite/test/ReactWrapper-spec.jsx b/packages/enzyme-test-suite/test/ReactWrapper-spec.jsx
index e426c9e83..306da0771 100644
--- a/packages/enzyme-test-suite/test/ReactWrapper-spec.jsx
+++ b/packages/enzyme-test-suite/test/ReactWrapper-spec.jsx
@@ -2460,6 +2460,68 @@ describeWithDOM('mount', () => {
});
});
+ it('prevents the update if nextState is null or undefined', () => {
+ class Foo extends React.Component {
+ constructor(props) {
+ super(props);
+ this.state = { id: 'foo' };
+ }
+
+ componentDidUpdate() {}
+
+ render() {
+ return (
+
+ );
+ }
+ }
+
+ const wrapper = mount();
+ const spy = sinon.spy(wrapper.instance(), 'componentDidUpdate');
+ const callback = sinon.spy();
+ wrapper.setState(() => ({ id: 'bar' }), callback);
+ expect(spy).to.have.property('callCount', 1);
+ expect(callback).to.have.property('callCount', 1);
+
+ wrapper.setState(() => null, callback);
+ expect(spy).to.have.property('callCount', is('>= 16') ? 1 : 2);
+ expect(callback).to.have.property('callCount', 2);
+
+ wrapper.setState(() => undefined, callback);
+ expect(spy).to.have.property('callCount', is('>= 16') ? 1 : 3);
+ expect(callback).to.have.property('callCount', 3);
+ });
+
+ itIf(is('>= 16'), 'prevents an infinite loop if nextState is null or undefined from setState in CDU', () => {
+ class Foo extends React.Component {
+ constructor(props) {
+ super(props);
+ this.state = { id: 'foo' };
+ }
+
+ componentDidUpdate() {}
+
+ render() {
+ return (
+
+ );
+ }
+ }
+
+ let payload;
+ const stub = sinon.stub(Foo.prototype, 'componentDidUpdate')
+ .callsFake(function componentDidUpdate() { this.setState(() => payload); });
+
+ const wrapper = mount();
+
+ wrapper.setState(() => ({ id: 'bar' }));
+ expect(stub).to.have.property('callCount', 1);
+
+ payload = null;
+ wrapper.setState(() => ({ id: 'bar' }));
+ expect(stub).to.have.property('callCount', 2);
+ });
+
describe('should not call componentWillReceiveProps after setState is called', () => {
it('should not call componentWillReceiveProps upon rerender', () => {
class A extends React.Component {
diff --git a/packages/enzyme-test-suite/test/ShallowWrapper-spec.jsx b/packages/enzyme-test-suite/test/ShallowWrapper-spec.jsx
index 93e9b2093..71b790ae5 100644
--- a/packages/enzyme-test-suite/test/ShallowWrapper-spec.jsx
+++ b/packages/enzyme-test-suite/test/ShallowWrapper-spec.jsx
@@ -2406,6 +2406,68 @@ describe('shallow', () => {
});
});
+ it('prevents the update if nextState is null or undefined', () => {
+ class Foo extends React.Component {
+ constructor(props) {
+ super(props);
+ this.state = { id: 'foo' };
+ }
+
+ componentDidUpdate() {}
+
+ render() {
+ return (
+
+ );
+ }
+ }
+
+ const wrapper = shallow();
+ const spy = sinon.spy(wrapper.instance(), 'componentDidUpdate');
+ const callback = sinon.spy();
+ wrapper.setState(() => ({ id: 'bar' }), callback);
+ expect(spy).to.have.property('callCount', 1);
+ expect(callback).to.have.property('callCount', 1);
+
+ wrapper.setState(() => null, callback);
+ expect(spy).to.have.property('callCount', is('>= 16') ? 1 : 2);
+ expect(callback).to.have.property('callCount', 2);
+
+ wrapper.setState(() => undefined, callback);
+ expect(spy).to.have.property('callCount', is('>= 16') ? 1 : 3);
+ expect(callback).to.have.property('callCount', 3);
+ });
+
+ itIf(is('>= 16'), 'prevents an infinite loop if nextState is null or undefined from setState in CDU', () => {
+ class Foo extends React.Component {
+ constructor(props) {
+ super(props);
+ this.state = { id: 'foo' };
+ }
+
+ componentDidUpdate() {}
+
+ render() {
+ return (
+
+ );
+ }
+ }
+
+ let payload;
+ const stub = sinon.stub(Foo.prototype, 'componentDidUpdate')
+ .callsFake(function componentDidUpdate() { this.setState(() => payload); });
+
+ const wrapper = shallow();
+
+ wrapper.setState(() => ({ id: 'bar' }));
+ expect(stub).to.have.property('callCount', 1);
+
+ payload = null;
+ wrapper.setState(() => ({ id: 'bar' }));
+ expect(stub).to.have.property('callCount', 2);
+ });
+
describe('should not call componentWillReceiveProps after setState is called', () => {
it('should not call componentWillReceiveProps upon rerender', () => {
class A extends React.Component {
diff --git a/packages/enzyme/src/ShallowWrapper.js b/packages/enzyme/src/ShallowWrapper.js
index 264a54597..8160100b9 100644
--- a/packages/enzyme/src/ShallowWrapper.js
+++ b/packages/enzyme/src/ShallowWrapper.js
@@ -129,6 +129,9 @@ function getAdapterLifecycles({ options }) {
return {
...lifecycles,
+ setState: {
+ ...lifecycles.setState,
+ },
...(componentDidUpdate && { componentDidUpdate }),
};
}
@@ -436,6 +439,7 @@ class ShallowWrapper {
if (arguments.length > 1 && typeof callback !== 'function') {
throw new TypeError('ReactWrapper::setState() expects a function as its second argument');
}
+
this.single('setState', () => {
withSetStateAllowed(() => {
const adapter = getAdapter(this[OPTIONS]);
@@ -446,6 +450,16 @@ class ShallowWrapper {
const prevProps = instance.props;
const prevState = instance.state;
const prevContext = instance.context;
+
+ const statePayload = typeof state === 'function'
+ ? state.call(instance, prevState, prevProps)
+ : state;
+
+ // returning null or undefined prevents the update in React 16+
+ // /~https://github.com/facebook/react/pull/12756
+ const maybeHasUpdate = !lifecycles.setState.skipsComponentDidUpdateOnNullish
+ || statePayload != null;
+
// When shouldComponentUpdate returns false we shouldn't call componentDidUpdate.
// so we spy shouldComponentUpdate to get the result.
let spy;
@@ -462,16 +476,17 @@ class ShallowWrapper {
// We don't pass the setState callback here
// to guarantee to call the callback after finishing the render
if (instance[SET_STATE]) {
- instance[SET_STATE](state);
+ instance[SET_STATE](statePayload);
} else {
- instance.setState(state);
+ instance.setState(statePayload);
}
if (spy) {
shouldRender = spy.getLastReturnValue();
spy.restore();
}
if (
- shouldRender
+ maybeHasUpdate
+ && shouldRender
&& !this[OPTIONS].disableLifecycleMethods
&& lifecycles.componentDidUpdate
&& lifecycles.componentDidUpdate.onSetState