Tutorial

Using the New Portal Feature in React

Published on November 3, 2017
author

Danny Hurlburt

Using the New Portal Feature in React

React v16 introduced a new feature called portals. The documentation states that:

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

Normally, a functional or a class component renders a tree of React elements (usually generated from JSX). The React element defines how the DOM of the parent component should look.

Prior to v16, only a few child types were allowed to be rendered:

  • null or false (means render nothing).
  • JSX.
  • React elements.
function Example(props) {
  return null;
}
function Example(props) {
  return false;
}
function Example(props) {
  return <p>Some JSX</p>;
}
function Example(props) {
  return React.createElement(
    'p',
    null,
    'Hand coded'
  );
}

In v16, more child types were made renderable:

  • Numbers (including Infinity and NaN).
  • Strings.
  • React portals.
  • An array of renderable children.

Full list of renderable children

function Example(props) {
  return 42;  // Becomes a text node.
}
function Example(props) {
  return 'The meaning of life.';  // Becomes a text node.
}
function Example(props) {
  return ReactDOM.createPortal(
    // Any valid React child type
    [
      'A string',
      <p>Some JSX</p>,
      'etc'
    ],
    props.someDomNode
  );
}

React portals are created by calling ReactDOM.createPortal. The first argument should be a renderable child. The second argument should be a reference to the DOM node where the renderable child will be rendered. ReactDOM.createPortal returns an object that is similar in nature to what React.createElement returns.

Note that createPortal is in the ReactDOM namespace and not the React namespace like createElement.

Some observant readers may have noticed that the ReactDOM.createPortal signature is the same as ReactDOM.render, which makes it easy to remember. However, unlike ReactDOM.render, ReactDOM.createPortal returns a renderable child which is used during the reconciliation process.

When to Use

React portals are very useful when a parent component has overflow: hidden declared or has properties that affect the stacking context and you need to visually ā€œbreak outā€ of its container. Some examples include dialogs, global message notifications, hovercards, and tooltips.

Event Bubbling Through Portals

The React documentation explains this very well.

Even though a portal can be anywhere in the DOM tree, it behaves like a normal React child in every other way. Features like context work exactly the same regardless of whether the child is a portal, as the portal still exists in the React tree regardless of its position in the DOM tree.

This includes event bubbling. 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.

This makes listening to events in your dialogs, hovercards, etc, as easy as if they were rendered in the same DOM tree as the parent component.

Example

In the following example, weā€™ll take advantage of React portals and its event bubbling feature.

The markup begins with the following.

<div class="PageHolder">
</div>
<div class="DialogHolder  is-empty">
  <div class="Backdrop"></div>
</div>
<div class="MessageHolder">
</div>

The .PageHolder div is where the main part of our application lives. The .DialogHolder div will be where any generated dialogs are rendered. The .MessageHolder div will be where any generated messages are rendered.

Because we want all dialogs to be visually above the main part of our application, the .DialogHolder div has z-index: 1 declared. This will create a new stacking context independent of .PageHolderā€™s stacking context.

Because we want all messages to be visually above any dialogs, the .MessageHolder div has z-index: 1 declared. This will create a sibling stacking context to the .DialogHolderā€™s stacking context. Although the z-index of the sibling stacking contexts have the same value, this will still render how we want due to the fact that .MessageHolder comes after .DialogHolder in the DOM tree.

The following CSS summarizes the necessary rules to establish the desired stacking context.

.PageHolder {
  /* Just use stacking context of parent element. */
  /* A z-index: 1 would still work here. */
}

.DialogHolder {
  position: fixed;
  top: 0; left: 0;
  right: 0; bottom: 0;
  z-index: 1;
}

.MessageHolder {
  position: fixed;
  top: 0; left: 0;
  width: 100%;
  z-index: 1;
}

The example will have a Page component which will be rendered into .PageHolder.

class Page extends React.Component { /* ... */ }

ReactDOM.render(
  <Page/>,
  document.querySelector('.PageHolder')
)

Because our Page component will be rendering dialogs and messages into the .DialogHolder and the .MessageHolder, respectively, it will need a reference to these holder divs at render-time. We have several options.

We could resolve the references to these holder divs before rendering the Page component and pass them as properties to the Page component.

let dialogHolder = document.querySelector('.DialogHolder');
let messageHolder = document.querySelector('.MessageHolder');

ReactDOM.render(
  <Page dialogHolder={dialogHolder} messageHolder={messageHolder}/>,
  document.querySelector('.PageHolder')
);

We could pass selectors to the Page component as properties and then resolve the references in componentWillMount for the initial render and re-resolve in componentWillReceiveProps if the selectors change.

class Page extends React.Component {

  constructor(props) {
    super(props);
    let { dialogHolder = '.DialogHolder',
          messageHolder = '.MessageHolder' } = props

    this.state = {
      dialogHolder,
      messageHolder,
    }
  }

  componentWillMount() {
    let state = this.state,
        dialogHolder = state.dialogHolder,
        messageHolder = state.messageHolder

    this._resolvePortalRoots(dialogHolder, messageHolder);
  }

  componentWillReceiveProps(nextProps) {
    let props = this.props,
        dialogHolder = nextProps.dialogHolder,
        messageHolder = nextProps.messageHolder

    if (props.dialogHolder !== dialogHolder ||
        props.messageHolder !== messageHolder
    ) {
      this._resolvePortalRoots(dialogHolder, messageHolder);
    }
  }

  _resolvePortalRoots(dialogHolder, messageHolder) {
    if (typeof dialogHolder === 'string') {
      dialogHolder = document.querySelector(dialogHolder)
    }
    if (typeof messageHolder === 'string') {
      messageHolder = document.querySelector(messageHolder)
    }
    this.setState({
      dialogHolder,
      messageHolder,
    })
  }

}

Now that we have ensured we will have DOM references for the portals, we can render the Page component with dialogs and messages.

Just like React elements, React portals are rendered based on the component properties and state. For this example, we will have two buttons. One will create dialog portals to be rendered in the dialog holder when clicked, and the other one will create message portals to be rendered in the message holder. We will keep references to these portals in the componentā€™s state which will be used in the render method.

class Page extends React.Component {
  // ...

  constructor(props) {
    super(props);
    let { dialogHolder = '.DialogHolder',
          messageHolder = '.MessageHolder' } = props

    this.state = {
      dialogHolder,
      dialogs: [],
      messageHolder,
      messages: [],
    }
  }

  render() {
    let state = this.state,
        dialogs = state.dialogs,
        messages = state.messages

    return (
      <div className="Page">
        <button onClick={evt => this.addNewDialog()}>
          Add Dialog
        </button>
        <button onClick={evt => this.addNewMessage()}>
          Add Message
        </button>
        {dialogs}
        {messages}
      </div>
    )
  }

  addNewDialog() {
    let dialog = ReactDOM.createPortal((
        <div className="Dialog">
          ...
        </div>
      ),
      this.state.dialogHolder
    )
    this.setState({
      dialogs: this.state.dialogs.concat(dialog),
    })
  }

  addNewMessage() {
    let message = ReactDOM.createPortal((
        <div className="Message">
          ...
        </div>
      ),
      this.state.messageHolder
    )
    this.setState({
      messages: this.state.messages.concat(message),
    })
  }


  // ...
}

To demonstrate that events will bubble from the React portal components up to the parent component, letā€™s add a click handler on the .Page div.

class Page extends React.Component {
  // ...

  render() {
    let state = this.state,
        dialogs = state.dialogs,
        messages = state.messages

    return (
      <div className="Page" onClick={evt => this.onPageClick(evt)}>
        ...
      </div>
    )
  }

  onPageClick(evt) {
    console.log(`${evt.target.className} was clicked!`);
  }

  // ...
}

When a dialog or a message is clicked, the onPageClick event handler will be called (as long as another handler did not stop propagation).

See a working example of the above demonstration.

šŸ‘‰ Use React portals when you run into overflow: hidden or stacking context issues!

Thanks for learning with the DigitalOcean Community. Check out our offerings for compute, storage, networking, and managed databases.

Learn more about our products

About the authors
Default avatar
Danny Hurlburt

author

While we believe that this content benefits our community, we have not yet thoroughly reviewed it. If you have any suggestions for improvements, please let us know by clicking the ā€œreport an issueā€œ button at the bottom of the tutorial.

Still looking for an answer?

Ask a questionSearch for more help

Was this helpful?
Ā 
Leave a comment
ļ»æ

This textbox defaults to using Markdown to format your answer.

You can type !ref in this text area to quickly search our full set of tutorials, documentation & marketplace offerings and insert the link!

Try DigitalOcean for free

Click below to sign up and get $200 of credit to try our products over 60 days!

Sign up

Join the Tech Talk
Success! Thank you! Please check your email for further details.

Please complete your information!

Featured on Community

Get our biweekly newsletter

Sign up for Infrastructure as a Newsletter.

Hollie's Hub for Good

Working on improving health and education, reducing inequality, and spurring economic growth? We'd like to help.

Become a contributor

Get paid to write technical tutorials and select a tech-focused charity to receive a matching donation.

Welcome to the developer cloud

DigitalOcean makes it simple to launch in the cloud and scale up as you grow ā€” whether you're running one virtual machine or ten thousand.

Learn more