Consider a component which needs to manage subcomponents that are not children in its own DOM tree, but must be added to the top-level document.

A typical example would be an autocompletion field that needs to show autocompletion matches in a floating menu below the input field. The floating menu necessarily needs to be added as a child of the document body element to escape any "overflow: hidden" constaints in the tree that would prevent it from showing. The floating menu needs to be removed after it's no longer used.

In such a case, the logical approach seems to be to mount a component in an arbitrary div, and later unmount it when it's no longer needed. However, this introduces an interesting state-flow problem when events are used to trigger such an unmount.

Here is an excerpt from my current code, to illustrate the problem:

componentDidUpdate: function(prevProps, prevState) {
  if (prevState.matches !== this.state.matches) {
    if (this._floater) {
      this._floater.remove();
      this._floater = null;
    }

    if (this.state.matches.length > 0) {
      this._floater = Floater.create(
        <Floater
          parentElement={this.getDOMNode()}
          open={true}>
          <SelectableList
            items={this.state.matches}
            limit={10}
            onSelectionChange={this.handleSelectionChange}/>
        </Floater>
      );
    }
  }
},

handleSelectionChange: function(items) {
  this.setState({matches: [], selectedItem: items[0]});
},

Here, Floater is a generic component that can contain any other component; it sets itself to absolute, positions itself and so on. Floater.create() is a convenience method to create a floater component and insert it into the document.

Floater.remove() currently looks like this:

remove: function() {
  var self = this;
  if (this.isMounted()) {
    window.setTimeout(function() {
      React.unmountComponentAtNode(self.getDOMNode().parentNode);
    }, 10);
  }
},

The reason it's using a timeout is to allow the parent component to be able to remote it after a state update. Selecting something in the SelectableList would trigger a handleSelectionChange in the parent, which would call remove() to unmount the component while it's still being used. It's ugly, although it does work.

Is there a better, more idiomatic way?

没有正确的解决方案

其他提示

Just a head-up for visitors to this question.

As of React v16, there is a specific feature to handle this very case. It is called Portal

Portals provide a first-class way to render children into a DOM node that exists outside the DOM hierarchy of the parent component.

许可以下: CC-BY-SA归因
不隶属于 StackOverflow