Skip to content

Commit cba495f

Browse files
koba04ljharb
authored andcommitted
[Fix] shallow: skip updates when nextState is null or undefined
- related: facebook/react#12756 Fixes enzymejs#1783.
1 parent 8bc3635 commit cba495f

File tree

3 files changed

+138
-3
lines changed

3 files changed

+138
-3
lines changed

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

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2460,6 +2460,68 @@ describeWithDOM('mount', () => {
24602460
});
24612461
});
24622462

2463+
it('prevents the update if nextState is null or undefined', () => {
2464+
class Foo extends React.Component {
2465+
constructor(props) {
2466+
super(props);
2467+
this.state = { id: 'foo' };
2468+
}
2469+
2470+
componentDidUpdate() {}
2471+
2472+
render() {
2473+
return (
2474+
<div className={this.state.id} />
2475+
);
2476+
}
2477+
}
2478+
2479+
const wrapper = mount(<Foo />);
2480+
const spy = sinon.spy(wrapper.instance(), 'componentDidUpdate');
2481+
const callback = sinon.spy();
2482+
wrapper.setState(() => ({ id: 'bar' }), callback);
2483+
expect(spy).to.have.property('callCount', 1);
2484+
expect(callback).to.have.property('callCount', 1);
2485+
2486+
wrapper.setState(() => null, callback);
2487+
expect(spy).to.have.property('callCount', is('>= 16') ? 1 : 2);
2488+
expect(callback).to.have.property('callCount', 2);
2489+
2490+
wrapper.setState(() => undefined, callback);
2491+
expect(spy).to.have.property('callCount', is('>= 16') ? 1 : 3);
2492+
expect(callback).to.have.property('callCount', 3);
2493+
});
2494+
2495+
itIf(is('>= 16'), 'prevents an infinite loop if nextState is null or undefined from setState in CDU', () => {
2496+
class Foo extends React.Component {
2497+
constructor(props) {
2498+
super(props);
2499+
this.state = { id: 'foo' };
2500+
}
2501+
2502+
componentDidUpdate() {}
2503+
2504+
render() {
2505+
return (
2506+
<div className={this.state.id} />
2507+
);
2508+
}
2509+
}
2510+
2511+
let payload;
2512+
const stub = sinon.stub(Foo.prototype, 'componentDidUpdate')
2513+
.callsFake(function componentDidUpdate() { this.setState(() => payload); });
2514+
2515+
const wrapper = mount(<Foo />);
2516+
2517+
wrapper.setState(() => ({ id: 'bar' }));
2518+
expect(stub).to.have.property('callCount', 1);
2519+
2520+
payload = null;
2521+
wrapper.setState(() => ({ id: 'bar' }));
2522+
expect(stub).to.have.property('callCount', 2);
2523+
});
2524+
24632525
describe('should not call componentWillReceiveProps after setState is called', () => {
24642526
it('should not call componentWillReceiveProps upon rerender', () => {
24652527
class A extends React.Component {

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

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2406,6 +2406,68 @@ describe('shallow', () => {
24062406
});
24072407
});
24082408

2409+
it('prevents the update if nextState is null or undefined', () => {
2410+
class Foo extends React.Component {
2411+
constructor(props) {
2412+
super(props);
2413+
this.state = { id: 'foo' };
2414+
}
2415+
2416+
componentDidUpdate() {}
2417+
2418+
render() {
2419+
return (
2420+
<div className={this.state.id} />
2421+
);
2422+
}
2423+
}
2424+
2425+
const wrapper = shallow(<Foo />);
2426+
const spy = sinon.spy(wrapper.instance(), 'componentDidUpdate');
2427+
const callback = sinon.spy();
2428+
wrapper.setState(() => ({ id: 'bar' }), callback);
2429+
expect(spy).to.have.property('callCount', 1);
2430+
expect(callback).to.have.property('callCount', 1);
2431+
2432+
wrapper.setState(() => null, callback);
2433+
expect(spy).to.have.property('callCount', is('>= 16') ? 1 : 2);
2434+
expect(callback).to.have.property('callCount', 2);
2435+
2436+
wrapper.setState(() => undefined, callback);
2437+
expect(spy).to.have.property('callCount', is('>= 16') ? 1 : 3);
2438+
expect(callback).to.have.property('callCount', 3);
2439+
});
2440+
2441+
itIf(is('>= 16'), 'prevents an infinite loop if nextState is null or undefined from setState in CDU', () => {
2442+
class Foo extends React.Component {
2443+
constructor(props) {
2444+
super(props);
2445+
this.state = { id: 'foo' };
2446+
}
2447+
2448+
componentDidUpdate() {}
2449+
2450+
render() {
2451+
return (
2452+
<div className={this.state.id} />
2453+
);
2454+
}
2455+
}
2456+
2457+
let payload;
2458+
const stub = sinon.stub(Foo.prototype, 'componentDidUpdate')
2459+
.callsFake(function componentDidUpdate() { this.setState(() => payload); });
2460+
2461+
const wrapper = shallow(<Foo />);
2462+
2463+
wrapper.setState(() => ({ id: 'bar' }));
2464+
expect(stub).to.have.property('callCount', 1);
2465+
2466+
payload = null;
2467+
wrapper.setState(() => ({ id: 'bar' }));
2468+
expect(stub).to.have.property('callCount', 2);
2469+
});
2470+
24092471
describe('should not call componentWillReceiveProps after setState is called', () => {
24102472
it('should not call componentWillReceiveProps upon rerender', () => {
24112473
class A extends React.Component {

packages/enzyme/src/ShallowWrapper.js

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -436,6 +436,7 @@ class ShallowWrapper {
436436
if (arguments.length > 1 && typeof callback !== 'function') {
437437
throw new TypeError('ReactWrapper::setState() expects a function as its second argument');
438438
}
439+
439440
this.single('setState', () => {
440441
withSetStateAllowed(() => {
441442
const adapter = getAdapter(this[OPTIONS]);
@@ -446,6 +447,15 @@ class ShallowWrapper {
446447
const prevProps = instance.props;
447448
const prevState = instance.state;
448449
const prevContext = instance.context;
450+
451+
const statePayload = typeof state === 'function'
452+
? state.call(instance, prevState, prevProps)
453+
: state;
454+
455+
// returning null or undefined prevents the update
456+
// https://github.com/facebook/react/pull/12756
457+
const maybeHasUpdate = statePayload != null;
458+
449459
// When shouldComponentUpdate returns false we shouldn't call componentDidUpdate.
450460
// so we spy shouldComponentUpdate to get the result.
451461
let spy;
@@ -462,16 +472,17 @@ class ShallowWrapper {
462472
// We don't pass the setState callback here
463473
// to guarantee to call the callback after finishing the render
464474
if (instance[SET_STATE]) {
465-
instance[SET_STATE](state);
475+
instance[SET_STATE](statePayload);
466476
} else {
467-
instance.setState(state);
477+
instance.setState(statePayload);
468478
}
469479
if (spy) {
470480
shouldRender = spy.getLastReturnValue();
471481
spy.restore();
472482
}
473483
if (
474-
shouldRender
484+
maybeHasUpdate
485+
&& shouldRender
475486
&& !this[OPTIONS].disableLifecycleMethods
476487
&& lifecycles.componentDidUpdate
477488
&& lifecycles.componentDidUpdate.onSetState

0 commit comments

Comments
 (0)