Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Changing state too quickly cause error with shallow render #7927

Closed
Sawtaytoes opened this issue Oct 10, 2016 · 2 comments · Fixed by #8097
Closed

Changing state too quickly cause error with shallow render #7927

Sawtaytoes opened this issue Oct 10, 2016 · 2 comments · Fixed by #8097

Comments

@Sawtaytoes
Copy link

Sawtaytoes commented Oct 10, 2016

Do you want to request a feature or report a bug?
Report a bug

What is the current behavior?
Running ReactTestUtils.createRenderer().render() causes react to throw an exception:

Warning: Exception thrown by hook while handling onSetChildren: Invariant Violation: Expected onBeforeMountComponent() parent and onSetChildren() to be consistent (3 has parents 0 and 2).
Invariant Violation: Expected onBeforeMountComponent() parent and onSetChildren() to be consistent (3 has parents 0 and 2).

I have the same issue when using both Component and PureComponent.
I do not see the error when using ReactTestUtils.renderToDocument().

If the current behavior is a bug, please provide the steps to reproduce and if possible a minimal demo of the problem via https://jsfiddle.net or similar (template: https://jsfiddle.net/reactjs/69z2wepo/).
Could not figure out how to use ReactTestUtils in that JSFiddle example even though react-with-addons is loaded.

Here's the code I used in my project to reproduce it:

import React, { Component, PureComponent } from 'react'
import { render } from 'react-dom'
import ReactTestUtils from 'react-addons-test-utils'

class Root extends Component {
    componentWillMount() {
        this.testBreak()
        setTimeout(this.testBreak.bind(this), 10)
    }

    testBreak() {
        var i = 0, l = 50
        while(i <= l) {
            this.setState({ test: `this will break testing ${++i}` })
        }
    }

    render() {
        return (
            <div>
                {/* You need more than one nested element for this to break */}
                <div>First div</div>
                <div>Second div</div>
            </div>
        )
    }
}

render(
    <Root />
, document.getElementById('root'))

class TestReactElement extends PureComponent {
    render() { return (
        <div>{this.props.name}</div>
    )}
}

const testRender = (name) => {
    let renderer = ReactTestUtils.createRenderer()
    renderer.render(<TestReactElement name={name} />)
}

testRender('Test 1')
testRender('Test 2')
testRender('Test 3')

It works fine so long as I run testRender() once. As soon as it's run 2 or more times, the error pops up.

In my project, TAP output is being listened to from a console.log wrapper and state is being updated when a new one comes in. So while my reproducible example is contrived, it's real-world bug when running tests in my project.

What is the expected behavior?
The expected behavior is that no error occurs in the console when running multiple shallow test renders while at the same time React is updating the state of an element rendered to the DOM.

Which versions of React, and which browser / OS are affected by this issue? Did this work in previous versions of React?
15.3.0 and 15.3.2. Windows 10 64-bit v1607 (and updates up to October 10th, 2016). I've not tried this in previous versions of React. Verified it displays the error in Chrome 54.0.2840.50 beta-m (64-bit) and Firefox 50.0b5.

@Sawtaytoes Sawtaytoes changed the title Too many calls to state change too quickly cause error with shallow render Changing state too quickly cause error with shallow render Oct 10, 2016
@goatslacker
Copy link
Contributor

goatslacker commented Oct 22, 2016

Here's an updated fail case with less code and which works in node:

'use strict'

const React = require('react')
const enzyme = require('enzyme')
const jsdom = require('jsdom')
const doc = jsdom.jsdom('<!doctype html><html><body></body></html>')
global.document = doc
global.window = doc.defaultView

class Root extends React.Component {
  componentWillMount() {
    // Will work if this is not deferred
    setTimeout(this.testBreak.bind(this), 1)
  }

  testBreak() {
    // Will work if we do not setState here
    this.setState({})
  }

  render() {
    // Will work if we remove at least one `a`. This bug manifests itself with multiple children.
    // It doesn't matter if the children are in an Array.
    return React.createElement('div', null, React.createElement('a'), React.createElement('a'))
  }
}

enzyme.mount(React.createElement(Root))

const TestReactElement = () => React.createElement('div')

// Will work if we use `mount` instead of shallow rendering
const testRender = () => enzyme.shallow(
  React.createElement(TestReactElement)
)

// Will work if we call testRender < 3x
testRender()
testRender()
testRender()

Edit: updated with code comments.

@goatslacker
Copy link
Contributor

goatslacker commented Oct 25, 2016

Here's another example that uses a pubsub event-emitter which simulates what a test would look like if it was hooked up to something like flux:

'use strict'

const React = require('react')
const enzyme = require('enzyme')
const jsdom = require('jsdom')
const transmitter = require('transmitter')

const bus = transmitter()

const doc = jsdom.jsdom('<!doctype html><html><body></body></html>')
global.document = doc
global.window = doc.defaultView

class Root extends React.Component {
  componentDidMount() {
    // Flux-like
    bus.subscribe(() => this.setState({}))
  }

  render() {
    // Will work if we remove at least one `a`. This bug manifests itself with multiple children.
    // It doesn't matter if the children are in an Array.
    return React.createElement('div', null, React.createElement('a'), React.createElement('i'))
  }
}

// Works if we use shallow rendering here
enzyme.mount(React.createElement(Root))

const TestReactElement = () => React.createElement('section')

// Will work if we use `mount` instead of shallow rendering
const testRender = () => enzyme.shallow(
  React.createElement(TestReactElement)
)

// Will work if we call testRender < 3x
testRender()
testRender()
testRender()

// Once we call an action in a separate test, it'll fail.
bus.publish(2)

@gaearon gaearon modified the milestones: 15-next, 15.4.0 Oct 25, 2016
@zpao zpao removed this from the 15.4.0 milestone Nov 16, 2016
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.

4 participants