Skip to content

Commit b048ce3

Browse files
committed
Fix to skip updates when nextState is null or undefined
1 parent 8bc3635 commit b048ce3

File tree

3 files changed

+127
-1
lines changed

3 files changed

+127
-1
lines changed

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

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

2463+
it('should prevent the update if the 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', 1);
2488+
// the callback should always be called
2489+
expect(callback).to.have.property('callCount', 2);
2490+
2491+
wrapper.setState(() => undefined, callback);
2492+
expect(spy).to.have.property('callCount', 1);
2493+
expect(callback).to.have.property('callCount', 3);
2494+
});
2495+
2496+
it('should prevent an infinite loop if the nextState is null or undefined from setState in CDU', () => {
2497+
class Foo extends React.Component {
2498+
constructor(props) {
2499+
super(props);
2500+
this.state = { id: 'foo' };
2501+
}
2502+
2503+
componentDidUpdate() {
2504+
// eslint-disable-next-line react/no-did-update-set-state
2505+
this.setState(() => null);
2506+
}
2507+
2508+
render() {
2509+
return (
2510+
<div className={this.state.id} />
2511+
);
2512+
}
2513+
}
2514+
2515+
const wrapper = mount(<Foo />);
2516+
const spy = sinon.spy(wrapper.instance(), 'componentDidUpdate');
2517+
wrapper.setState(() => ({ id: 'bar' }));
2518+
expect(spy).to.have.property('callCount', 1);
2519+
});
2520+
24632521
describe('should not call componentWillReceiveProps after setState is called', () => {
24642522
it('should not call componentWillReceiveProps upon rerender', () => {
24652523
class A extends React.Component {

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

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

2409+
it('should prevent the update if the 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', 1);
2434+
// the callback should always be called
2435+
expect(callback).to.have.property('callCount', 2);
2436+
2437+
wrapper.setState(() => undefined, callback);
2438+
expect(spy).to.have.property('callCount', 1);
2439+
expect(callback).to.have.property('callCount', 3);
2440+
});
2441+
2442+
it('should prevent an infinite loop if the nextState is null or undefined from setState in CDU', () => {
2443+
class Foo extends React.Component {
2444+
constructor(props) {
2445+
super(props);
2446+
this.state = { id: 'foo' };
2447+
}
2448+
2449+
componentDidUpdate() {
2450+
// eslint-disable-next-line react/no-did-update-set-state
2451+
this.setState(() => null);
2452+
}
2453+
2454+
render() {
2455+
return (
2456+
<div className={this.state.id} />
2457+
);
2458+
}
2459+
}
2460+
2461+
const wrapper = shallow(<Foo />);
2462+
const spy = sinon.spy(wrapper.instance(), 'componentDidUpdate');
2463+
wrapper.setState(() => ({ id: 'bar' }));
2464+
expect(spy).to.have.property('callCount', 1);
2465+
});
2466+
24092467
describe('should not call componentWillReceiveProps after setState is called', () => {
24102468
it('should not call componentWillReceiveProps upon rerender', () => {
24112469
class A extends React.Component {

packages/enzyme/src/ShallowWrapper.js

Lines changed: 11 additions & 1 deletion
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,14 @@ class ShallowWrapper {
446447
const prevProps = instance.props;
447448
const prevState = instance.state;
448449
const prevContext = instance.context;
450+
451+
if (typeof state === 'function') {
452+
state = state.call(instance, prevState, prevProps);
453+
}
454+
// returning null or undefined prevents the update
455+
// https://github.com/facebook/react/pull/12756
456+
const hasUpdate = state !== null && state !== undefined;
457+
449458
// When shouldComponentUpdate returns false we shouldn't call componentDidUpdate.
450459
// so we spy shouldComponentUpdate to get the result.
451460
let spy;
@@ -471,7 +480,8 @@ class ShallowWrapper {
471480
spy.restore();
472481
}
473482
if (
474-
shouldRender
483+
hasUpdate
484+
&& shouldRender
475485
&& !this[OPTIONS].disableLifecycleMethods
476486
&& lifecycles.componentDidUpdate
477487
&& lifecycles.componentDidUpdate.onSetState

0 commit comments

Comments
 (0)