Enzyme: setState getting into infinite loop

Created on 21 Aug 2018  ·  8Comments  ·  Source: enzymejs/enzyme

A component that fires setState at componentDidMount and componentDidUpdate enters in an infinite loop. Here in a simplified example that produces this situation.
Notice:

  • with versions 3.4.0 and 3.4.1 is OK.
  • since 3.4.2 it breaks.
  • commenting out 1 of the lifecycle calls, it pass.
  • same code with mount also breaks.

Interesting:

  • wrapping the 1 of the 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"

Most helpful comment

I found an issue that enzyme doesn't handle PureComponent well. I'll fix it.

The PR is #1786

All 8 comments

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:

  • "Component", it works by commenting 1 method, or wrapping the call in setTimeout.

Fix for React: https://codesandbox.io/s/0mr5y064l

  • "PureComponent", it just works as no change is detected (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

Was this page helpful?
0 / 5 - 0 ratings