Rendering children outside their parent


(Ed Schiebel) #1

I am working on popup menus. I have a MenuButton that displays a Menu when clicked. The natural approach is to have the Menu a child of the MenuButton

 <MenuButton>
        <Menu> ... </Menu>
    </MenuButton>

but if the MenuButton or any of its ancestors is not overflow:visible there’s a real chance that the Menu is clipped. What I really want is to render the Menu in the body (or the app’s top-level element), absolutely positioned adjacent to its parent button.

I’ve done searching and reading on the topic, looked at a variety of existing implementations,

  • a single menu container component at the application level that gets passed its menu data as props.
  • a render-in-the-body component that wraps the Menu, renders nothing, but calls React.render at various points in its lifecycle.
  • an onClick handler that creates the menu React element and calls React.render, but it requires having a pre-arranged div in the DOM to act as the container node.

but none of them feel like a proper React approach and each fails in one way or another. This seems like a case where it would be so much easier to just reach in there and manipulate the DOM, but I don’t want to do that.

Is there a best practice for rendering components outside their parent?
Or, is there a way to define the Menu outside its controlling MenuButton that I’m not seeing?


(Kaare Skovgaard) #2

What we use is something akin to what is described here: http://joecritchley.svbtle.com/portals-in-reactjs

It allows you to “Portal” a React subtree into other places in the tree.

Effectively allowing the MenuButton component to render it’s items in the body-element when the menu is open.

We have a slightly different implementation at Secoya (where I work) which also sends down updates when the owning component is rerendered.

Our implementation looks like this:

var Portal = React.createClass({
    mixins: [React.addons.PureRenderMixin],
    propTypes: {
      portal: React.PropTypes.string
    },
    mountPortal: function(portal) {
      var target;
      target = this.resolveTarget(portal);
      this.target = document.createElement('div');
      this.target.style.display = 'inline-block';
      if (target == null) {
        document.body.appendChild(this.target);
      } else {
        target.appendChild(this.target);
      }
      return this.renderPortal(this.target);
    },
    unmountPortal: function(portal) {
      var target;
      target = this.resolveTarget(portal);
      React.unmountComponentAtNode(this.target);
      this.component = null;
      if (target != null) {
        target.removeChild(this.target);
      } else {
        document.body.removeChild(this.target);
      }
      return this.target = null;
    },
    componentDidMount: function() {
      return this.mountPortal(this.props.portal);
    },
    componentWillUnmount: function() {
      return this.unmountPortal(this.props.portal);
    },
    resolveTarget: function(portal) {
      var el;
      if (portal != null) {
        el = document.getElementById(portal);
        if (el == null) {
          throw new Error("Could not find portal with id " + portal + "!");
        }
        return el;
      }
      return null;
    },
    renderPortal: function(target) {
      var el;
      el = React.createElement("div", null, this.props.children);
      return this.component = React.render(el, target);
    },
    updatePortal: function() {
      return this.component.setProps({
        children: this.props.children
      });
    },
    componentDidUpdate: function(prevProps, prevState) {
      if (prevProps.portal === this.props.portal) {
        return this.updatePortal();
      } else {
        this.unmountPortal(prevProps.portal);
        return this.mountPortal(this.props.portal);
      }
    },
    render: function() {
      return null;
    }
  });

I realize there’s some bad variable names in there. It’s still a work in progress, but I hope you can use it for inspiration.


(Kier Borromeo) #3

I was thinking how you’d test a component that uses a Portal, @kastermester?


(Michal Dúbravčík) #4

Portal is also implement in Semantic UI.
See https://react.semantic-ui.com/addons/portal#portal-example-portal