Comments

How to use React Portals

In React, the default behavior is to have your entire app rendered under a single DOM node (the app root). Most of the times, this is not a problem. However, sometimes, it would be useful if you could append content under DOM nodes outside your app root. And React has a great way to do this: portals. So, do you want to render children outside your app root DOM node? Keep reading and you’ll learn how.

The problem

This is a minimal boot file for a typical React application:

import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';

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

And here’s a standard index.html file for such app:

<!DOCTYPE html>

<html lang="en">
<head>
  <meta charset="utf-8" />
  <meta
    name="viewport"
    content="width=device-width, initial-scale=1, shrink-to-fit=no"
    />
  <title>React App</title>
</head>
<body>
  <div id="root"></div>
</body>
</html>

In many applications, having a whole tree of elements under a single DOM node will never be a problem. Even so, sooner or later you’ll come across a situation where you need to render a component outside your app’s root. A common case where this is necessary is when you have some CSS-related issue in a node somewhere above in the hierarchy (some properties that often cause such situations: z-index, overflow and position). Components like modals, floating menus, popovers, toasts, chat widgets and tooltips could benefit from having their own rendering root.

React portals

Portals are React’s official solution to solve the above problem. It’s a way to render children into any DOM node outside your app’s root. As the name implies, it simply “teleports” elements to another point, like a sibling node to the app root node, for example. This extracted part of your app will behave normally, still under React’s control.

An example

To demonstrate how portals work, let’s implement a no-op chat widget.

Firstly, we add a new root in the index.html file, as a sibling to the app root node. This is where our widget component will be rendered:

<body>
  <div id="app-root"></div>
  <div id="chat-widget-root"></div>
</body>

Now, let’s create the component file for this widget. I named it chat-widget.js:

import React, { Component } from 'react';
import ReactDOM from 'react-dom';
import './chat-widget.css';

const chatWidgetRoot = document.getElementById('chat-widget-root');

export class ChatWidget extends Component {
  state = { open: false };
  
  handleOpenButtonClick = () => this.setState({ open: true });

  handleCloseButtonClick = () => this.setState({ open: false });

  render() {
    return ReactDOM.createPortal(this.renderWidget(), chatWidgetRoot);
  }

  renderWidget() { /*...*/ }
}

This is what is happening in the above code:

  1. First, we import the ReactDOM module. It will be used to render the portal itself.
  2. Second, we query the DOM to get a reference to the rendering root of our portal.
  3. Then, we initialize our component’s state object. It has a single property, open, that we’ll use to control whether the chat is open or not.
  4. Next, we declare a pair of click event handlers to control the opening state of our widget.
  5. After that, in the component’s render method, we finally create the portal. This is done by calling the ReactDOM.createPortal method, passing the children as the first argument and the target root DOM node as the second one. The widget’s structure was intentionally extracted to a separate method, which we’ll see below.

Now, take a look at the renderWidget method:

renderWidget() {
  return (
    <>
      { !this.state.open && (
          <button className="chat__open-button" onClick={this.handleOpenButtonClick}>Need help?</button>
        )
      }
      { this.state.open && (
          <div className="chat__window">
            <header className="chat__header">
              <h3 className="chat__title">Live support</h3>
              <button className="chat__close-button" onClick={this.handleCloseButtonClick}>
                <img src="/arrow-down-icon.svg" alt=""/>
              </button>
            </header>
            <div className="chat__messages"/>
            <textarea className="chat__message-input" placeholder="Type your message here"/>
          </div>
        )
      }
    </>
  );
}

The above code contains the markup of our widget and has no direct relation to the portal rendering logic itself. We also control the toggling of the chat window in this method, by using the open state property. Note that we’re using a pair of empty tags (<> and </>) to wrap the component’s markup. This is a syntactic sugar for Fragments, a React feature to group multiple children without adding a new node to the DOM. As this is a new syntax, maybe your React toolset doesn’t support it yet. But don’t worry, you could safely replace it with Fragment:

import React, { Component, Fragment } from 'react';
/*...*/

export class ChatWidget extends Component {
  /*...*/

  renderWidget() {
    return (
      <Fragment>
        {/*...*/}
      </Fragment>
    );
  }
}

To verify that the portal works, you just need to import the ChatWidget component and render it anywhere in your app. In our example, we put it in the ForgotPassword component:

import React from 'react';
import { ChatWidget } from './chat-widget';
import './forgot-password.css';

export default () => (
  <div className="forgot-password">
    <h1 className="app-external__title">Recover your account</h1>
    <form>
      <input type="text" placeholder="Enter the last password you remember..."/>
      <button onClick={e => e.preventDefault()}>Continue</button>
    </form>
    <ChatWidget/>
  </div>
);

And finally, here’s our component working:

DOM tree vs React tree

There are two distinct trees to be aware of when using portals:

  1. The DOM tree: this is the hierarchy of nodes that your application (and every other web page) renders. It exists only in memory and not necessarily will be identical to your original HTML. To be precise, if you’re using any modern JavaScript framework (like Angular) or library (like React), your DOM tends to be a lot different from your HTML. Probably 99.9% of your whole application will be rendered dynamically, at runtime, having only a small static HTML file to load it. You can inspect your app’s DOM by using the developer tools in your favorite browser. When you use portals, children moves within the DOM.
  2. The React tree: this is the hierarchy of components in your app. It will never be affected by the use of portals. If you have a component X that is a child of a component Y, but is rendered somewhere outside your app root node, it will only affect your DOM, nothing else. X will still be a child of Y. Even the events in a “teleported” child will propagate normally to its parents.

React portals: our example works!
Conclusion

  • Portals are the official React way to render children outside the root node of your app.
  • Children rendered through portals are still under React’s control.
  • Portals only affect the DOM structure, never the React components tree.
  • Event bubbling will work normally between components rendered into external nodes and their parent components (in the React tree).

If you really want to master React, you need to check out Mosh’s React course. It’s the best React course out there! And if you liked this article, share it with others as well!

 

JavaScript hacker, front-end engineer and F/OSS lover.
Tags: ,

Leave a Reply

Connect with Me
  • Categories
  • Popular Posts