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).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:
Infinity
and NaN
).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.
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.
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.
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 div
s at render-time. We have several options.
We could resolve the references to these holder div
s 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.
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.
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!