为什么从setState回调触发ReactDom.render()返回null?

时间:2018-11-20 10:20:30

标签: javascript reactjs react-dom

我遇到了react的这种非常奇怪的行为,并在下面创建了一个简单的演示。

其行为是,当我像第182行一样直接执行this.renderPortal()时,可以关闭对话框。但是何时从setState回调中执行以下操作:

return <div onClick={()=>{
  // this.renderPortal() //worker fine 
  this.setState({
    a : 1
  }, ()=>{
    this.renderPortal()
  })
}}> click me</div>

我无法关闭它,控制台将提示Cannot read property 'destroy' of null

似乎React认为从setState回调呈现的Dialog是无状态的,为什么?

let rootDialogContainer = document.createElement("div");
document.body.appendChild(rootDialogContainer);
function createConfig(config, defaults) {
  return Object.assign(
    {},
    defaults,
    typeof config === "string" ? { content: config } : config
  );
}
class Modal extends React.Component {
  constructor(props) {
    super(props);

    this.state = {
      show: true
    };
    this.cancel = this.cancel.bind(this);

  }

  ok() {
    if (this.props.autoClose) {
      this.setState({ show: false });
    }

    const { onOk, afterClose } = this.props;
    onOk && onOk();
    afterClose();
  }

  cancel() {
    if (this.mounted && this.props.autoClose) {
      this.setState({ show: false });
    }
    const { onCancel, afterClose } = this.props;
    onCancel && onCancel();
    afterClose();
  }

  destroy() {
    this.props.forceClose();
  }

  componentDidMount() {
    this.mounted = true
  }



  componentWillUnmount() {
    this.mounted = false
  }

  render() {
    let DialogType = this.props.dialog || Dialog;
    return (
      <DialogType
        {...this.props}
        show={this.state.show}
        onOk={this.ok}
        onCancel={this.cancel}
      />
    );
  }
}
class Dialog extends React.Component{
  constructor(props) {
    super(props)
  }
  renderDialogWrap(params) {
    let {      
      contentElm,
      onCancel
    } = params;




    return (
      <div className={"dialog"}>
        <div style={{}} className={"inner-dialog"}>     
          {contentElm}
          <div className="button" onClick={onCancel}>
            cancel
          </div>
        </div>
      </div>
    );
  }

  renderPortal() {
    return <Modal />;
  }
  render() {
    let props = this.props;
    let {
      show,
      className,
      title,
      content,
      onCancel = () => {},
      onOk = () => {},           
      children,
      renderAsHiddenIfNotShow = false,
    } = props;
    return ReactDOM.createPortal(
      <div
        style={{ display: show ? "" : "none" }}
     
      >
        {this.renderDialogWrap({
          onCancel,
          contentElm: children,          
          show,
          renderAsHiddenIfNotShow
        })}
      </div>,
      document.body
    ); 
  }
}
Dialog.show = function(params) {
  const config = createConfig(params, {
    show: true,
    autoClose: true,    
    onOk: () => {},
    onCancel: () => {}
  });

  config.content = config.content || config.desc;

  let container = rootDialogContainer


  if (config.id) {
   
    let containerId = `wrapper`
    container = document.getElementById(containerId)
    if (!container) {
      container = document.createElement('div')
      container.setAttribute('id', containerId)
      document.body.appendChild(container)
    }
  }

  config.forceClose = function(){
    ReactDOM.unmountComponentAtNode(container);
  };

  config.afterClose = function() {
    config.autoClose && config.forceClose();
  };

  return ReactDOM.render(this.buildModal(config), container);;
};
class Wrapper extends React.Component{
  constructor(props){
    super(props)
  }
  renderPortal(){
    const destroy = () => {
      this.myDialog.destroy();
    };

    this.myDialog = Dialog.show({
      onOk: ()=>{},
      onCancel: destroy,
      onClose: destroy,
      autoClose: false,
      content:  <div>
        <p>Test</p>
      </div>
    });  
  }
  render(){
    return <div onClick={()=>{
      // this.renderPortal() //worker fine 
      this.setState({
        a : 1
      }, ()=>{
        this.renderPortal()
      })
    }}> click me</div>
  }
}
Dialog.buildModal = function(config) {
  return <Modal {...config} />;
};

function App() {

  return (
    <div className="App">
      <Wrapper />
    </div>
  );
}

const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
.dialog{
    HEIGHT: 50px;
    background: white;
    border:1px solid gray;
    top: 20px;
    width: 500px;
    position: absolute;

}
.inner-dialog{
    width: 100%;
    height: 100%;
}
.button{
    position: absolute;
    bottom: 0;
    border:1px solid gray;
}
<div id="root"></div>
<script crossorigin src="https://unpkg.com/react@16/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script>

1 个答案:

答案 0 :(得分:-1)

原因很简单,虽然对话框本身是无状态的,但在呈现有状态的组件时会呈现它,因此ReactDom.render会将其视为有状态的。

绕过此问题的简单技巧是替换:

this.setState({
    a : 1
  }, ()=>{
    this.renderPortal()
  })
})

this.setState({
    a : 1
  }, ()=>{
    setTimeout(() => {
      this.renderPortal()
    }, 1)
  })

通过这种方式,您的组件被视为无状态。

完整代码:

let rootDialogContainer = document.createElement("div");
document.body.appendChild(rootDialogContainer);
function createConfig(config, defaults) {
  return Object.assign(
    {},
    defaults,
    typeof config === "string" ? { content: config } : config
  );
}
class Modal extends React.Component {
  constructor(props) {
    super(props);

    this.state = {
      show: true
    };
    this.cancel = this.cancel.bind(this);

  }

  ok() {
    if (this.props.autoClose) {
      this.setState({ show: false });
    }

    const { onOk, afterClose } = this.props;
    onOk && onOk();
    afterClose();
  }

  cancel() {
    if (this.mounted && this.props.autoClose) {
      this.setState({ show: false });
    }
    const { onCancel, afterClose } = this.props;
    onCancel && onCancel();
    afterClose();
  }

  destroy() {
    this.props.forceClose();
  }

  componentDidMount() {
    this.mounted = true
  }



  componentWillUnmount() {
    this.mounted = false
  }

  render() {
    let DialogType = this.props.dialog || Dialog;
    return (
      <DialogType
        {...this.props}
        show={this.state.show}
        onOk={this.ok}
        onCancel={this.cancel}
      />
    );
  }
}
class Dialog extends React.Component{
  constructor(props) {
    super(props)
  }
  renderDialogWrap(params) {
    let {      
      contentElm,
      onCancel
    } = params;




    return (
      <div className={"dialog"}>
        <div style={{}} className={"inner-dialog"}>     
          {contentElm}
          <div className="button" onClick={onCancel}>
            cancel
          </div>
        </div>
      </div>
    );
  }

  renderPortal() {
    return <Modal />;
  }
  render() {
    let props = this.props;
    let {
      show,
      className,
      title,
      content,
      onCancel = () => {},
      onOk = () => {},           
      children,
      renderAsHiddenIfNotShow = false,
    } = props;
    return ReactDOM.createPortal(
      <div
        style={{ display: show ? "" : "none" }}
     
      >
        {this.renderDialogWrap({
          onCancel,
          contentElm: children,          
          show,
          renderAsHiddenIfNotShow
        })}
      </div>,
      document.body
    ); 
  }
}
Dialog.show = function(params) {
  const config = createConfig(params, {
    show: true,
    autoClose: true,    
    onOk: () => {},
    onCancel: () => {}
  });

  config.content = config.content || config.desc;

  let container = rootDialogContainer


  if (config.id) {
   
    let containerId = `wrapper`
    container = document.getElementById(containerId)
    if (!container) {
      container = document.createElement('div')
      container.setAttribute('id', containerId)
      document.body.appendChild(container)
    }
  }

  config.forceClose = function(){
    ReactDOM.unmountComponentAtNode(container);
  };

  config.afterClose = function() {
    config.autoClose && config.forceClose();
  };

  return ReactDOM.render(this.buildModal(config), container);;
};
class Wrapper extends React.Component{
  constructor(props){
    super(props)
  }
  renderPortal(){
    const destroy = () => {
      this.myDialog.destroy();
    };

    this.myDialog = Dialog.show({
      onOk: ()=>{},
      onCancel: destroy,
      onClose: destroy,
      autoClose: false,
      content:  <div>
        <p>Test</p>
      </div>
    });  
  }
  render(){
    return <div onClick={()=>{
      // this.renderPortal() //worker fine 
      this.setState({
        a : 1
      }, ()=>{
        setTimeout(() => this.renderPortal(), 1)
      })
    }}> click me</div>
  }
}
Dialog.buildModal = function(config) {
  return <Modal {...config} />;
};

function App() {

  return (
    <div className="App">
      <Wrapper />
    </div>
  );
}

const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
.dialog{
    HEIGHT: 500px;
    background: white;
    border:1px solid gray;
    top: 20px;
    width: 500px;
    position: absolute;

}
.inner-dialog{
    width: 100%;
    height: 100%;
}
.button{
    position: absolute;
    bottom: 0;
    border:1px solid gray;
}
<div id="root"></div>
<script crossorigin src="https://unpkg.com/react@16/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script>

Updated demo