How to create a React Modal (which is appended to <body>) with transitions?

JavascriptReactjsModal DialogCss Transitions

Javascript Problem Overview


There is a modal in this answer https://stackoverflow.com/a/26789089/883571 which is creating a React-based Modal by appending it to <body>. However, I found it not compatible with the transition addons provided by React.

How to create one with transitions(during enter and leave)?

Javascript Solutions


Solution 1 - Javascript

At react conf 2015, Ryan Florence demonstrated using portals. Here's how you can create a simple Portal component...

var Portal = React.createClass({
  render: () => null,
  portalElement: null,
  componentDidMount() {
    var p = this.props.portalId && document.getElementById(this.props.portalId);
    if (!p) {
      var p = document.createElement('div');
      p.id = this.props.portalId;
      document.body.appendChild(p);
    }
    this.portalElement = p;
    this.componentDidUpdate();
  },
  componentWillUnmount() {
    document.body.removeChild(this.portalElement);
  },
  componentDidUpdate() {
    React.render(<div {...this.props}>{this.props.children}</div>, this.portalElement);
  }
});

and then everything you can normally do in React you can do inside of the portal...

    <Portal className="DialogGroup">
       <ReactCSSTransitionGroup transitionName="Dialog-anim">
         { activeDialog === 1 && 
            <div key="0" className="Dialog">
              This is an animated dialog
            </div> }
       </ReactCSSTransitionGroup>
    </Portal> 

jsbin demo

You can also have a look at Ryan's react-modal, although I haven't actually used it so I don't know how well it works with animation.

Solution 2 - Javascript

I wrote the module react-portal that should help you.

Usage:

import { Portal } from 'react-portal';
 
<Portal>
  This text is portaled at the end of document.body!
</Portal>
 
<Portal node={document && document.getElementById('san-francisco')}>
  This text is portaled into San Francisco!
</Portal>

Solution 3 - Javascript

React 15.x

Here's an ES6 version of the method described in this article:

import React from 'react';
import ReactDOM from 'react-dom';
import PropTypes from 'prop-types';

export default class BodyEnd extends React.PureComponent {
    
    static propTypes = {
        children: PropTypes.node,
    };
    
    componentDidMount() {
        this._popup = document.createElement('div');
        document.body.appendChild(this._popup);
        this._render();
    }

    componentDidUpdate() {
        this._render();
    }

    componentWillUnmount() {
        ReactDOM.unmountComponentAtNode(this._popup);
        document.body.removeChild(this._popup);
    }

    _render() {
        ReactDOM.render(this.props.children, this._popup);
    }
    
    render() {
        return null;
    }
}

Just wrap any elements you want to be at the end of the DOM with it:

<BodyEnd><Tooltip pos={{x,y}}>{content}</Tooltip></BodyEnd>

React 16.x

Here's an updated version for React 16:

import React from 'react';
import ReactDOM from 'react-dom';

export default class BodyEnd extends React.Component {

    constructor(props) {
        super(props);
        this.el = document.createElement('div');
        this.el.style.display = 'contents';
        // The <div> is a necessary container for our
        // content, but it should not affect our layout.
        // Only works in some browsers, but generally
        // doesn't matter since this is at
        // the end anyway. Feel free to delete this line.
    }
    
    componentDidMount() {
        document.body.appendChild(this.el);
    }

    componentWillUnmount() {
        document.body.removeChild(this.el);
    }
    
    render() {
        return ReactDOM.createPortal(
            this.props.children,
            this.el,
        );
    }
}

Working example

Solution 4 - Javascript

As other answers have stated this can be done using Portals. Starting from v16.0 Portals are included in React.

<body>
  <div id="root"></div>
  <div id="portal"></div>
</body>

Normally, when you return an element from a component's render method, it's mounted into the DOM as a child of the nearest parent node, but with portals you can insert a child into a different location in the DOM.

const PortalComponent = ({ children, onClose }) => {
  return createPortal(
    <div className="modal" style={modalStyle} onClick={onClose}>
      {children}
    </div>,
    // get outer DOM element
    document.getElementById("portal")
  );
};

class App extends React.Component {
  constructor(props) {
    super(props);

    this.state = {
      modalOpen: false
    };
  }

  render() {
    return (
      <div style={styles}>
        <Hello name="CodeSandbox" />
        <h2>Start editing to see some magic happen {"\u2728"}</h2>
        <button onClick={() => this.setState({ modalOpen: true })}>
          Open modal
        </button>
        {this.state.modalOpen && (
          <PortalComponent onClose={() => this.setState({ modalOpen: false })}>
            <h1>This is modal content</h1>
          </PortalComponent>
        )}
      </div>
    );
  }
}

render(<App />, document.getElementById("root"));

Check working example here.

Solution 5 - Javascript

The fundamental problem here is that in React you're only allowed to mount component to its parent, which is not always the desired behavior. But how to address this issue?

I've made the solution, addressed to fix this issue. More detailed problem definition, src and examples can be found here: https://github.com/fckt/react-layer-stack#rationale

> ### Rationale react/react-dom comes comes with 2 basic assumptions/ideas: >

  • every UI is hierarchical naturally. This why we have the idea of components which wrap each other > - react-dom mounts (physically) child component to its parent DOM node by default > > The problem is that sometimes the second property isn't what you want > in your case. Sometimes you want to mount your component into > different physical DOM node and hold logical connection between > parent and child at the same time. > > Canonical example is Tooltip-like component: at some point of > development process you could find that you need to add some > description for your UI element: it'll render in fixed layer and > should know its coordinates (which are that UI element coord or > mouse coords) and at the same time it needs information whether it > needs to be shown right now or not, its content and some context from > parent components. This example shows that sometimes logical hierarchy > isn't match with the physical DOM hierarchy.

Take a look at https://github.com/fckt/react-layer-stack/blob/master/README.md#real-world-usage-example to see the concrete example which is answer to your question:

import { Layer, LayerContext } from 'react-layer-stack'
// ... for each `object` in array of `objects`
  const modalId = 'DeleteObjectConfirmation' + objects[rowIndex].id
  return (
    <Cell {...props}>
        // the layer definition. The content will show up in the LayerStackMountPoint when `show(modalId)` be fired in LayerContext
        <Layer use={[objects[rowIndex], rowIndex]} id={modalId}> {({
            hideMe, // alias for `hide(modalId)`
            index } // useful to know to set zIndex, for example
            , e) => // access to the arguments (click event data in this example)
          <Modal onClick={ hideMe } zIndex={(index + 1) * 1000}>
            <ConfirmationDialog
              title={ 'Delete' }
              message={ "You're about to delete to " + '"' + objects[rowIndex].name + '"' }
              confirmButton={ <Button type="primary">DELETE</Button> }
              onConfirm={ this.handleDeleteObject.bind(this, objects[rowIndex].name, hideMe) } // hide after confirmation
              close={ hideMe } />
          </Modal> }
        </Layer>

        // this is the toggle for Layer with `id === modalId` can be defined everywhere in the components tree
        <LayerContext id={ modalId }> {({showMe}) => // showMe is alias for `show(modalId)`
          <div style={styles.iconOverlay} onClick={ (e) => showMe(e) }> // additional arguments can be passed (like event)
            <Icon type="trash" />
          </div> }
        </LayerContext>
    </Cell>)
// ...

Solution 6 - Javascript

I think this code is more or less self explanatory and covers the core solution of what most people are looking for:

ReactDOM.render(
  <Modal />,
  document.body.appendChild( document.createElement( 'div' ) ),
)

Solution 7 - Javascript

I've written a library to help with this. I avoid the DOM insertion hacks used by Portal strategies out there and instead make use of context based registries to pass along components from a source to a target.

My implementation makes use of the standard React render cycles. The components that you teleport/inject/transport don't cause a double render cycle on the target - everything happens synchronously.

The API is also structured in a manner to discourage the use of magic strings in your code to define the source/target. Instead you are required to explicitly create and decorate components that will be used as the target (Injectable) and the source (Injector). As this sort of thing is generally considered quite magical I think explicit Component representation (requiring direct imports and usage) may help alleviate confusion on where a Component is being injected.

Although my library won't allow you to render as a direct child of the document.body you can achieve an acceptable modal effect by binding to a root level component in your component tree. I plan on adding an example of this use case soon.

See https://github.com/ctrlplusb/react-injectables for more info.

Solution 8 - Javascript

Hope it helps. This is my current implementation of a transition modal based on the anwser above:

  React = require 'react/addons'

  keyboard = require '../util/keyboard'
  mixinLayered = require '../mixin/layered'

  $ = React.DOM
  T = React.PropTypes
  cx = React.addons.classSet

  module.exports = React.createFactory React.createClass
    displayName: 'body-modal'
    mixins: [mixinLayered]

    propTypes:
      # this components accepts children
      name:             T.string.isRequired
      title:            T.string
      onCloseClick:     T.func.isRequired
      showCornerClose:  T.bool
      show:             T.bool.isRequired

    componentDidMount: ->
      window.addEventListener 'keydown', @onWindowKeydown

    componentWillUnmount: ->
      window.removeEventListener 'keydown', @onWindowKeydown

    onWindowKeydown: (event) ->
      if event.keyCode is keyboard.esc
        @onCloseClick()

    onCloseClick: ->
      @props.onCloseClick()

    onBackdropClick: (event) ->
      unless @props.showCornerClose
        if event.target is event.currentTarget
          @onCloseClick()

    renderLayer: ->
      className = "body-modal is-for-#{@props.name}"
      $.div className: className, onClick: @onBackdropClick,
        if @props.showCornerClose
          $.a className: 'icon icon-remove', onClick: @onCloseClick
        $.div className: 'box',
          if @props.title?
            $.div className: 'title',
              $.span className: 'name', @props.title
              $.span className: 'icon icon-remove', @onCloseClick
          @props.children

    render: ->
      $.div()

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionjiyinyiyongView Question on Stackoverflow
Solution 1 - JavascriptGil BirmanView Answer on Stackoverflow
Solution 2 - JavascripttajoView Answer on Stackoverflow
Solution 3 - JavascriptmpenView Answer on Stackoverflow
Solution 4 - JavascriptKristaps TaubeView Answer on Stackoverflow
Solution 5 - JavascriptfcktView Answer on Stackoverflow
Solution 6 - JavascriptItay GrudevView Answer on Stackoverflow
Solution 7 - JavascriptctrlplusbView Answer on Stackoverflow
Solution 8 - JavascriptjiyinyiyongView Answer on Stackoverflow