Portals: Event Bubbling

// These two containers are siblings in the DOM
const appRoot = document.getElementById('app-root');
const modalRoot = document.getElementById('modal-root');

class Modal extends React.Component {
  constructor(props) {
    this.el = document.createElement('div');

  componentDidMount() {
    // The portal element is inserted in the DOM tree after
    // the Modal's children are mounted, meaning that children
    // will be mounted on a detached DOM node. If a child
    // component requires to be attached to the DOM tree
    // immediately when mounted, for example to measure a
    // DOM node, or uses 'autoFocus' in a descendant, add
    // state to Modal and only render the children when Modal
    // is inserted in the DOM tree.

  componentWillUnmount() {

  render() {
    return ReactDOM.createPortal(

class Parent extends React.Component {
  constructor(props) {
    this.state = {clicks: 0};
    this.handleClick = this.handleClick.bind(this);

  handleClick() {
    // This will fire when the button in Child is clicked,
    // updating Parent's state, even though button
    // is not direct descendant in the DOM.
    this.setState(state => ({
      clicks: state.clicks + 1

  render() {
    return (
      <div onClick={this.handleClick}>
        <p>Number of clicks: {this.state.clicks}</p>
          Open up the browser DevTools
          to observe that the button
          is not a child of the div
          with the onClick handler.
          <Child />

function Child() {
  // The click event on this button will bubble up to parent,
  // because there is no 'onClick' attribute defined
  return (
    <div className="modal">

ReactDOM.render(<Parent />, appRoot);

An event fired from inside a portal will propagate to ancestors in the containing React tree, even if those elements are not ancestors in the DOM tree.

Related concepts

Event Bubbling

Portals: Event Bubbling — Structure map

Clickable & Draggable!

Portals: Event Bubbling — Related pages: