React Portal is a scheme for rendering child nodes to DOM nodes other than their parent components. – the React document

By default, when a component’s Render method returns an element, it is attached to the nearest DOM node, its parent. Such as this

class Item extends React.Component {
    // ...
    render() {
        return (
            <div>xxx</div>); }}class App extends React.Component {
    // ...
    render() {
        return (
            <div className="wrap">
              <Item />
            </div>); }}Copy the code

The Item component is mounted on a div node whose className is “wrap”, and the content returned by the Item is rendered within the region rendered by the App component.

But sometimes we want to use child components within the parent component, but the render content in the child component does not appear in the parent component’s render area, but elsewhere, or even the DOM node mounted is not in the parent component’s child node.

Requirements as shown below:

  • The Button area is a component whose contents are displayed differently depending on the navigation;
  • The Button component also renders differently depending on which Tab (Basic Info, Deployment Configuration, Permission Assignment) page is currently active in the content area
  • Button area components and content area components are not parent-child components, but siblings

An implementation that does not use a portal

The key to this implementation is that each Tab page defines a Button area component, which is positioned to the specified location using CSS absolute positioning.

This ensures that the buttons in the Button area change as the tabs in the content area switch, since they are themselves mounted on the DOM node of the Tab page below.

However, since the visual position of the rendering is changed by absolute positioning, the style relationship between the parent node and its siblings needs to be sorted out. For example, the content area cannot set overflow: hidden; Style; There may also be other position: Relative (or Absolute) elements between the Tab component and the button area, and so on.

Use the portal

The React Portal usage

ReactDOM.createPortal(child, container)
Copy the code

This method is defined in react-dom rather than react

A child is the content that is passed to render. It is any renderable React child element, such as an element, string, or fragment.

React does not create a new div. It simply renders the child elements of the first argument into “Container”. “Container” is a valid DOM node that can be anywhere. As shown in the official example

  • Through the firstdocument.createElementCreate one that is not mounted anywheredivThe element
  • Will thisdivElements byappendChildMethod to the specified DOM node
  • Then thedivElements ascreatePortalThe second argument to render the child element into thisdivRender the content you want to render in the specified position.

Let’s take an official example:

/ / entry index. HTML<! DOCTYPE html><html>
  <body>
    <div id="app-root"></div>
    <div id="modal-root"></div>
  </body>
</html>

// Component implementation
import React from 'react';
import ReactDOM from 'react-dom';

// Get two DOM nodes
const appRoot = document.getElementById('app-root');
const modalRoot = document.getElementById('modal-root');

// Create a modal component that is an implementation of the Portal API
class Modal extends React.Component {
  constructor(props) {
    super(props);
    // Create a div to which we will render Modal. Because each modal component has its own element,
    // So we can render multiple modal components into modal containers.
    this.el = document.createElement('div');
  }

  componentDidMount() {
    // Append the element to the DOM on mount. We will render to modal container elements
    modalRoot.appendChild(this.el);
  }

  componentWillUnmount() {
    // Remove the manually created DOM when uninstalling the component
    modalRoot.removeChild(this.el);
  }
  
  render() {
    // Use teleporter to render children into the element
    return ReactDOM.createPortal(
      // Any valid React child node: JSX, strings, arrays, etc
      this.props.children,
      / / DOM elements
      this.el, ); }}The // Modal component is a normal React component, so we can render it anywhere and the user does not need to know that it is implemented using a portal.
class App extends React.Component {
  constructor(props) {
    super(props);
    this.state = {showModal: false};
    
    this.handleShow = this.handleShow.bind(this);
    this.handleHide = this.handleHide.bind(this);
  }

  handleShow() {
    this.setState({showModal: true});
  }
  
  handleHide() {
    this.setState({showModal: false});
  }

  render() {
    // Display Modal when clicked
    const modal = this.state.showModal ? (
      <Modal>
        <div className="modal">
          xxx
          <button onClick={this.handleHide}>Hide modal</button>
        </div>
      </Modal>
    ) : null;

    return (
      <div className="app">
        This div has overflow: hidden.
        <button onClick={this.handleShow}>Show modal</button>
        {modal}
      </div>
    );
  }
}

ReactDOM.render(<App />, appRoot);
Copy the code

PS: Here’s a question: why not just pass in the target DOM node as the second parameter?

In practice, passing the target DOM node directly is perfectly fine (see here, the official example modification for Fork). But the problem is, as described, the second argument can be anywhere. This means that there is no guarantee that the DOM node passed in has been rendered, so manual verification is required in case the target DOM has not been rendered at the time of transfer.

With the basics, the general idea is:

  • First, set up the DOM structure of the Button area and Content area
  • Create a generic ButtonPortal component that accepts the content to be rendered through props. Children, and then sends it using a portal and mounts it to the DOM element used for placeholder in the Buttons area
  • There are three Tabs in the Content component, and each Tab Panel is a separate component. The different panels call the ButtonPortal component and pass rendered button information to ButtonPortal via props

Once you have the idea, you can write the code structure according to the idea.

import React, { useEffect, useRef, useState } from "react";
import ReactDOM from "react-dom";
import "./styles.css";

function Target(props) {
  const modalRoot = document.getElementById("modal-root");
  const eleRef = useRef(document.createElement("div"));

  useEffect(() = > {
    if (modalRoot) {
      modalRoot.appendChild(eleRef.current);
      return () = > {
        if(modalRoot) { modalRoot.removeChild(eleRef.current); }}; } }, [modalRoot]);return ReactDOM.createPortal(
    <div>
      hello world!
      {props.children}
    </div>,
    eleRef.current
  );
}

function View() {
  const [show, setShow] = useState(false);
  return (
    <div>The content will be delivered to the red area above<button onClick={()= >SetShow (true)}> Enable transmission</button>
      {show && (
        <Target>
          <div className="modal">
            <div>Through Portal, we can render content to different parts of the DOM as if it were any other React child.</div>
            <button onClick={()= >SetShow (false)}> Destroy the target</button>
          </div>
        </Target>
      )}
    </div>
  );
}
export default function App() {
  return (
    <div className="app">
      <div id="modal-root"></div>
      <View />
    </div>
  );
}
Copy the code

Event bubbling occurs on the Portal

Examples of official documentation

Although a Portal can be placed anywhere in the DOM tree, it behaves like a normal React child node. Context functions, event bubbling, and so on.

In the case of event bubblings, (after React V16) events that are triggered inside the Portal rendered DOM bubble all the way to the source location where the transfer is enabled (not where the DOM is actually rendered mounted). For example, in the official documentation example, the Parent component in #app-root can catch uncaught events bubbling up from the sibling node #modal-root.


The following is from Morgan Cheng’s React Portal:

Why does React need a portal?

React Portal is called Portal because it does exactly the same thing as “Portal” : Render into a component that actually changes the DOM structure of another part of the page.

For example, what if a component needs to display a Dialog during rendering under certain conditions? A typical use case for Portal is when the parent component has overflow: Hidden or Z-index style, but the child component needs to be able to visually “jump” out of its container. Examples are dialog boxes, hover cards, and prompt boxes.

React portal implementation prior to V16

Prior to V16, implementing portals used two secret React apis

  • unstable_renderSubtreeIntoContainer
  • unmountComponentAtNode

First unstable_renderSubtreeIntoContainer, all take the prefix unstable, knew are not encouraged to use, but can’t, don’t have to use, also good React has not deprecate the API, Until V16 directly supported Portal. The purpose of this API is to set up “portals”, where components represented by JSX are stuffed inside the portal and rendered on the other side of the portal.

The second unmountComponentAtNode is used to clean up the side effects of the first API. It is usually called when unmounted, otherwise it will cause resource leakage.

A generic Dialog component implementation looks something like this. Note the comments in renderPortal.

The React Portal implementation prior to V16 had one minor flaw: Portal was one-way, content was routed through Portal to another exit, and events that occurred on that exit DOM did not bubble back to the other end.

import React from 'react';
import {unstable_renderSubtreeIntoContainer, unmountComponentAtNode} 
  from 'react-dom';

class Dialog extends React.Component {
  render() {
    return null;
  }

  componentDidMount() {
    const doc = window.document;
    this.node = doc.createElement('div');
    doc.body.appendChild(this.node);

    this.renderPortal(this.props);
  }

  componentDidUpdate() {
    this.renderPortal(this.props);
  }

  componentWillUnmount() {
    unmountComponentAtNode(this.node);
    window.document.body.removeChild(this.node);
  }

  renderPortal(props) {
    unstable_renderSubtreeIntoContainer(
      this.// represents the current component
      <div class="dialog">
        {props.children}
      </div>.// JSX inserted into the portal
      this.node // The DOM node on the other side of the portal); }}Copy the code
  1. First of all,renderFunctions should not return meaningful onesJSXThis component doesn’t draw anything through its normal life cycle. If it does, the drawn HTML/DOM will appear directly in the Dialog, which is not what we want.
  2. incomponentDidMountInside, use native API to use inbodyCreate adivthedivThe style of the element is never disturbed by the style of other elements.
  3. And then, no mattercomponentDidMountorcomponentDidUpdate, both call onerenderPortalStuff going through the portal.

To summarize, the Dialog component does this:

  • It doesn’t draw anything for itself,renderReturns anullIt is enough;
  • What it does is it callsrenderPortalDraw the object in a different corner of the DOM tree.

reference

  • Portals
  • React Portal – Cheng Mo