A component that fires setState at componentDidMount and componentDidUpdate enters in an infinite loop. Here in a simplified example that produces this situation.
Notice:
mount also breaks.Interesting:
setState with asetTimeout WORKS.js
componentDidUpdate() {
setTimeout(() => {
this.setState({value: false});
});
}
Example test:
import React, {Component} from 'react';
import {shallow} from 'enzyme';
class Example extends Component {
constructor(props: any) {
super(props);
this.state = {
value: false,
};
}
componentDidMount() {
this.setState({value: false});
}
componentDidUpdate() {
this.setState({value: false});
}
render() {
return React.createElement('div', null, this.state.value);
}
}
it('should not go crazy', () => {
expect(() => {
shallow(React.createElement(Example));
}).not.toThrow();
});
With "enzyme": "3.4.0" and "enzyme": "3.4.1":
PASS src/components/example.test.ts
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
With "enzyme": "3.4.2":
FAIL src/components/example.test.ts
● should not go crazy
expect(function).not.toThrow()
Expected the function not to throw an error.
Instead, it threw:
Method “setState” is only meant to be run on a single node. undefined found instead.
24 | }
25 |
> 26 | it('should not go crazy', () => {
| ^
27 | expect(() => {
28 | shallow(React.createElement(Example));
29 | }).not.toThrow();
at ShallowWrapper.single (node_modules/enzyme/build/ShallowWrapper.js:1718:17)
at ShallowWrapper.setState (node_modules/enzyme/build/ShallowWrapper.js:499:14)
at Example.ShallowWrapper.instance.setState (node_modules/enzyme/build/ShallowWrapper.js:184:33)
at Example.componentDidMount (src/components/example.test.ts:26:10)
at node_modules/enzyme/build/ShallowWrapper.js:189:20
at Object.batchedUpdates (node_modules/enzyme-adapter-react-16/build/ReactSixteenAdapter.js:392:22)
at new ShallowWrapper (node_modules/enzyme/build/ShallowWrapper.js:188:24)
at shallow (node_modules/enzyme/build/shallow.js:21:10)
at src/components/example.test.ts:46:25
at Object.<anonymous> (node_modules/expect/build/to_throw_matchers.js:51:9)
at Object.throwingMatcher [as toThrow] (node_modules/expect/build/index.js:320:33)
at Object.<anonymous> (src/components/example.test.ts:47:10)
With "enzyme": "3.4.3" and "enzyme": "3.4.4":
FAIL src/components/example.test.ts
● should not go crazy
expect(function).not.toThrow()
Expected the function not to throw an error.
Instead, it threw:
RangeError: Maximum call stack size exceeded
30 | });
31 |
at withSetStateAllowed (node_modules/enzyme/build/Utils.js:296:21)
at ShallowWrapper.<anonymous> (node_modules/enzyme/build/ShallowWrapper.js:511:42)
at ShallowWrapper.single (node_modules/enzyme/build/ShallowWrapper.js:1735:25)
at ShallowWrapper.setState (node_modules/enzyme/build/ShallowWrapper.js:510:14)
at Example.ShallowWrapper.instance.setState (node_modules/enzyme/build/ShallowWrapper.js:198:33)
at Example.componentDidUpdate (src/components/example.test.ts:32:10)
at node_modules/enzyme/build/ShallowWrapper.js:548:28
at withSetStateAllowed (node_modules/enzyme/build/Utils.js:300:3)
at ShallowWrapper.<anonymous> (node_modules/enzyme/build/ShallowWrapper.js:511:42)
at ShallowWrapper.single (node_modules/enzyme/build/ShallowWrapper.js:1735:25)
at ShallowWrapper.setState (node_modules/enzyme/build/ShallowWrapper.js:510:14)
at Example.ShallowWrapper.instance.setState (
---
REPEATED REPEATED REPEATED
---
node_modules/enzyme/build/ShallowWrapper.js:198:33)
at Example.componentDidUpdate (src/components/example.test.ts:32:10)
at node_modules/enzyme/build/ShallowWrapper.js:548:28
at withSetStateAllowed (node_modules/enzyme/build/Utils.js:300:3)
at ShallowWrapper.<anonymous> (node_modules/enzyme/build/ShallowWrapper.js:511:42)
at ShallowWrapper.single (node_modules/enzyme/build/ShallowWrapper.js:1735:25)
at ShallowWrapper.setState (node_modules/enzyme/build/ShallowWrapper.js:510:14)
at Example.ShallowWrapper.instance.setState (node_modules/enzyme/build/ShallowWrapper.js:198:33)
at Example.componentDidUpdate (src/components/example.test.ts:32:10)
Expected behavior
No loop.
Additional context
"jest": "^23.5.0"
"react": "^16.4.2"
"enzyme": "3.4.4"
"enzyme-adapter-react-16": "^1.2.0"
I think this is a duplicate of #1783.
@ljharb It could be that #1783 and #1784 are related. I just added some workarounds too, might be helpful to find the culprit.
cc @koba04 on this one too
And actually, your case makes infinite loop even in react. Could you check it in the react sandbox?
@Ailrun yep, infinite loop happening in React, when:
setTimeout.Fix for React: https://codesandbox.io/s/0mr5y064l
shouldComponentUpdate).Nevetheless, after changing the type of component to PureComponent it continues to break in test with Enzyme. Might be that Enzyme if firing the event without checking shouldComponentUpdate?
The component in our codebase is a PureComponent, tested thousand of times in last 2 years.
What is "strange", is that I discover this by debugging why a test that has being working for a life, started to fail.
IMO, then, your issue is about checking shouldComponentUpdate, not about setState, componentDidMount and componentDidUpdate combinations.
@artola
After enzyme v3.4.2, componentDidUpdate is always called if options.disableLifecycleMethods isn't enabled.
In this case, the followling didn't invoke componentDidUpdate before v3.4.2 so the update doesn't cause the infinite loop.
componentDidMount() {
this.setState({value: false});
}
componentDidUpdate() {
this.setState({value: false});
}
But this code would cause an inifinite loop with react-dom so I think this is not a bug for enzyme.
"PureComponent", it just works as no change is detected (shouldComponentUpdate).
I found an issue that enzyme doesn't handle PureComponent well. I'll fix it.
Thanks!
I found an issue that enzyme doesn't handle PureComponent well. I'll fix it.
The PR is #1786
Most helpful comment
The PR is #1786