Skip to content

Batching can update one component twice, causing unpredictable lifecycle #2410

Closed
@m4tthumphrey

Description

@m4tthumphrey

This one is hard to explain or reproduce but I'll try...

I'm using the following mixin which has changed slightly from the original I found in a gist a while back:

var React = require('react');

var LayeredComponentMixin = {

  componentDidMount: function() {
    this._layer = document.createElement('div');
    document.body.appendChild(this._layer);
    this._renderLayer();
  },

  componentDidUpdate: function() {
    this._renderLayer();
  },

  componentWillUnmount: function() {
    this._unrenderLayer();
    document.body.removeChild(this._layer);
  },

  _renderLayer: function() {
    var layer = this.renderLayer();

    if (null === layer) {
      layer = <noscript />;
    }

    React.render(layer, this._layer);

    if (this.layerDidMount) {
      this.layerDidMount(this._layer);
    }
  },

  _unrenderLayer: function() {
    if (this.layerWillUnmount) {
      this.layerWillUnmount(this._layer);
    }

    React.unmountComponentAtNode(this._layer);
  }

};

module.exports = LayeredComponentMixin;

This allows me to create layered components, in my case a modal dialog. I use the following component to build my modals:

var React = require('react');

var ToolboxUserActionCreators = require('../../actions/ToolboxUserActionCreators');

var ModalStore = require('../../stores/ModalStore');

function getStateFromStore() {
  return {
    modalCount: ModalStore.getModalCount()
  };
}

var Modal = React.createClass({

  componentWillMount: function() {
    this.setState({
      modalLevel: ModalStore.getModalCount()
    });
  },

  componentDidMount: function() {
    ModalStore.addChangeListener(this.onChange);
    document.addEventListener('keydown', this.handleKeyDown);
  },

  componentWillUnmount: function() {
    ModalStore.removeChangeListener(this.onChange);
    document.removeEventListener('keydown', this.handleKeyDown);
  },

  getInitialState: function() {
    return getStateFromStore();
  },

  getDefaultProps: function() {
    return {
      className: 'feature'
    }
  },

  render: function() {
    var className               = 'toolbox2-modal-content toolbox2-modal-' + this.props.className;
    var modalBackdropClassName  = 'toolbox2-modal-backdrop';
    var handleBackdropClick     = this.handleBackdropClick;
    var killClick               = this.killClick;
    var modalLevel              = this.state.modalLevel;
    var totalModals             = this.state.modalCount;

    if (modalLevel < totalModals) {
      modalBackdropClassName += ' toolbox2-modal-backdrop-secondary';
    }

    return (
      <div className={modalBackdropClassName} onClick={handleBackdropClick}>
        <div className={className} onClick={killClick}>
          {this.props.children}
        </div>
      </div>
    );
  },

  killClick: function(e) {    
    e.stopPropagation();
  },

  handleBackdropClick: function() {
    this.props.onRequestClose();
  },

  handleKeyDown: function(e) {
    if (e.keyCode === 27) {
      this.handleBackdropClick();
    }
  },

  onChange: function() {
    this.setState(getStateFromStore());
  }

});

module.exports = Modal;

This combination works perfectly. That is until I close the highest modal. If an <input /> component is used inside of the <Modal> render method, ie this.props.children, the following error is thrown. All other element types (that I've tried including textarea and select work fine, but input throws the following:

Uncaught Error: Invariant Violation: getDOMNode(): A component must be mounted to have a DOM node

The trace is as follows:

Uncaught Error: Invariant Violation: getDOMNode(): A component must be mounted to have a DOM node. bundle.js:60783
invariant bundle.js:60783
ReactBrowserComponentMixin.getDOMNode bundle.js:62069
ReactCompositeComponent.createClass.componentDidUpdate bundle.js:64334
assign.notifyAll bundle.js:68718
ON_DOM_READY_QUEUEING.close bundle.js:72258
Mixin.closeAll bundle.js:68960
Mixin.perform bundle.js:68901
Mixin.perform bundle.js:68887
assign.perform bundle.js:56107
(anonymous function) bundle.js:56186
wrapper bundle.js:53734
Mixin.closeAll bundle.js:68960
Mixin.perform bundle.js:68901
ReactDefaultBatchingStrategy.batchedUpdates bundle.js:64050
batchedUpdates bundle.js:56122
ReactEventListener.dispatchEvent bundle.js:64980

ReactCompositeComponent.createClass.componentDidUpdate bundle.js:64334 is here.

This only happens when there are several modals on top of each other; one dialog with an <input /> runs fine with no error.

The weird thing is that the app still runs fine, the error thrown has no impact (apart from being thrown) on how my app performs....

This is on 0.12RC1 using webpack to build.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions