Portals

Portals allow us to render components in DOM nodes which are outside of the parent-node of the current component hierarchy but still have access to the current component environment. A common example (but of course not the only one) is an overlay which is rendered in its own <div> outside of the actual application.

The portal remains in the context of the component that has created it and thus has access to all data that is also available to the parent component such as its props and state. However, they are placed in entirely different locations in the rendered HTML compared to the rest of the application. Being able to access props and state is crucial for Portals, as they allow us to access common context such as translations.

Creating portals

Creating a portal is relatively simple compared to other concepts we have learned about so far. The component intended to use the portal has to call the createPortal() method from ReactDOM and pass in a valid component as the first and an existing destination node as the second parameter.

The following example shows a common HTML snippet using portals:

<!doctype html>
<html>
<head>
<title>Portals in React</title>
</head>
<body>
<div id="root"><!-- this is where our React App is located --></div>
<div id="portal"><!-- this is where the content of the portal will be stored --></div>
</body>
</html>

While this snippet shows the corresponding React App:

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

const App = () => {
  return (
    <div>
      <h1>Portals in React</h1>
    </div>
  );
};

ReactDOM.render(<App />, document.querySelector('#root'));

As our <App /> is placed into the div with the id of root, the <body> of the app would now result in this HTML snippet:

<body>
  <div id="root">
    <div>
      <h1>Portals in React</h1>
    </div>
  </div>
  <div id="portal"><!-- this is where the content of the portal will be stored --></div>
</body>

Each additional component or each additional HTML element which is used in the JSX of our App component would end up in the div with the id="root". If we are dealing with a Portal however, the code would resemble the following:

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

const PortalExample = () => {
  return ReactDOM.createPortal(
    <div>Portal says Hello</div>,
    document.querySelector('#portal')
  );
};

Here, we can see the createPortal() method in action: first, we indicate which type of JSX should be rendered and second, we pass in the type of container where the JSX should be rendered into. Let's place this PortalExample component into our App:

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

const App = () => {
  return (
    <div>
      <h1>Portals in React</h1>
      <PortalExample />
    </div>
  );
};

ReactDOM.render(<App />, document.querySelector('#root'));

The resulting <body> in the HTML document will look like this:

<body>
  <div id="root">
    <div>
      <h1>Portals in React</h1>
    </div>
  </div>
  <div id="portal">
    <div>Portal says Hello</div>
  </div>
</body>

The Portal is rendered into the #portal node instead of the #root node where all other content including the component itself is placed. A Portal is rendered once the component mounts and is removed from the DOM if the component containing the portal is removed from the component tree.

Portals and their relationship to their parent component

In order to further our understanding of portals, we are going to build — surprise surprise — a modal portal. The basis is formed by the same HTML which we have used in the introduction of portals before. There are two divs in the example: one in which our application is rendered and another in which we render the portal.

This time however, the modal will only open once a user has clicked a button. The portal will contain a button which allows the user to close the window. A state variable called modalIsOpen is used to alternate between the two states and is either true or false. The ModalPortal component will be rendered via an && conditional in JSX, thus it is only shown if this.state.modalIsOpen is true.

During the time in which the value of the state changes from false to true, the ModalPortal component is mounted and the ModalPopup is rendered into the <div id="portal"> with a slightly transparent background. Once the value changes from true to false again, the ModalPortal is removed from the App component in the component tree. React takes care to ensure that the ModalPortal component and its contents are not found on the page anymore.

In code form, we are left with the following example:

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

const ModalPortal = (props) => {
  return ReactDOM.createPortal(
    <div
      style={{
        background: 'rgba(0,0,0,0.7)',
        height: '100vh',
        left: 0,
        position: 'fixed',
        top: 0,
        width: '100vw',
      }}
    >
      <div style={{ background: 'white', margin: 16, padding: 16 }}>
        {props.children}
      </div>
    </div>,
    document.getElementById('portal')
  );
};

class App extends React.Component {
  state = {
    modalIsOpen: false,
  };

  openModal = () => {
    this.setState({ modalIsOpen: true });
  };

  closeModal = () => {
    this.setState({ modalIsOpen: false });
  };

  render() {
    return (
      <div>
        <h1>Portals in React</h1>
        <button onClick={this.openModal}>Open Modal</button>
        {this.state.modalIsOpen && (
          <ModalPortal>
            <p>This text is opened in a Portal.</p>
            <button onClick={this.closeModal}>Close Modal</button>
          </ModalPortal>
        )}
      </div>
    );
  }
}
ReactDOM.render(<App />, document.getElementById('root'));

We need to pay special attention to the this.closeModal() method. Even though this method is defined in the App component, it is called within the ModalPortal component in the context of the App component once a user has clicked on the button "Close Modal".

This method can also alter the state of component via modalIsOpen even though the component is not placed within <div id="root"> as the rest of the components. Portals allow us to do this as the content is placed within the same component tree within React. The resulting HTML however, is different and the code is placed in a different <div> to the rest of the application.

Last updated