JoshWComeau

InPortal

Filed under
Snippets
on
in
September 17th, 2020.
Sep 2020.
import React from 'react';
import ReactDOM from 'react-dom';

const InPortal = ({ id, children }) => {
  const [hostElement, setHostElement] = React.useState(null);

  React.useEffect(() => {
    const elm = id
      ? document.querySelector(`#${id}`)
      : document.createElement('div');

    if (!elm) {
      return;
    }

    setHostElement(elm);

    if (!id) {
      document.body.appendChild(elm);
    }

    return () => {
      if (!id) {
        // In development, this error often masks other errors when a
        // hot reload occurs. We can safely ignore this.
        try {
          document.body.removeChild(elm);
        } catch (err) {
        }
      }
    };
  }, [id]);

  if (!hostElement) {
    return null;
  }

  return ReactDOM.createPortal(children, hostElement);
};

export default InPortal;

Link to this headingContext

React components are meant to be reusable and generic, but HTML doesn't always play so nice. The same tooltip component might work fine in one place, but be obstructed in another:

<div>
  {/* Works fine */}
  <Modal  />
</div>

<div style={{ overflow: 'hidden'}}>
  {/* Might be cut off! */}
  <Modal  />
</div>

For fixed floating components—tooltips, dropdowns, modals—this is a big problem! We want these components to work wherever we render them.

Thankfully, React has a solution for this: Portals(opens in new tab). In essence, this API lets a component render in a different part of the DOM tree.

The API is a bit funky though, especially when server-rendering or pre-generating (like with Gatsby or Next); portals can't be hydrated(opens in new tab). This has led to a lot of confusion.

This component wraps over all of the confusing bits, and provides an easy-to-use reusable abstraction.

Link to this headingUsage

Wrap your component with our InPortal component:

<div style={{ overflow: 'hidden'}}>
  <InPortal>
    <Modal  />
  </InPortal>
</div>

The InPortal component will automatically create a container node, appended to the end of the <body>. If you would prefer to manage this yourself, you can add a target container to the DOM, within the HTML. This implementation will vary depending on how you're using React, but here's a basic example for Vite/Parcel/Create React App:

<!-- public/index.html -->
<body>
  <!-- Your main application here: -->
  <div id="root"></div>

  <!-- Our new portal container here: -->
  <div id="portal-container"></div>
</body>

Then, whenever you need to render something into a portal, you use it like so:

<InPortal id="portal-container">
  Your content here!
</InPortal>

Link to this headingPositioning elements

When we use portals, our elements are rendered in a completely separate part of the DOM.

For things like modals, which are typically centered within the viewport, this is not a problem. But how about if you're building a tooltip, a dropdown, or something else that needs to be positioned relative to another element?

It's beyond the scope of this snippet, but in these cases, you'll need to use the getBoundingClientRect method, and then use absolute or fixed positioning to align the element with its target.

Link to this headingAccessibility

One issue with using portals generally is that focus becomes unintuitive; for folks who navigate with the keyboard, you'll want to make sure that the content in the portal can be reached.

The exact solution depends on what you're building, so it's outside the scope of this snippet. But be mindful! Make sure that your application is usable without a mouse.

Last updated on

September 17th, 2020

# of hits