createPortal does not overwrite div contents (like ReactDOM.render)

Viewed 4736

I am trying to get ReactDOM.createPortal to override the contents of the container I am mounting it too. However it seems to appendChild.

Is it possible to override contents? Similar to ReactDOM.render?

Here is my code:

import React from 'react';
import { createPortal } from 'react-dom';

class PrivacyContent extends React.Component {

    render() {
        return createPortal(
            <div>
                <button onClick={this.handleClick}>
                    Click me
                </button>
            </div>,
            document.getElementById('privacy')
        )
    }

    handleClick() {
        alert('clicked');
    }

}

export default PrivacyContent;
3 Answers

If you know what you're doing, here is a <Portal /> component that under the hoods creates a portal, empties the target DOM node and mounts any component with any props:

const Portal = ({ Component, container, ...props }) => {
  const [innerHtmlEmptied, setInnerHtmlEmptied] = React.useState(false)
  React.useEffect(() => {
    if (!innerHtmlEmptied) {
      container.innerHTML = ''
      setInnerHtmlEmptied(true)
    }
  }, [innerHtmlEmptied])
  if (!innerHtmlEmptied) return null
  return ReactDOM.createPortal(<Component {...props} />, container)
}

Usage:

<Portal Component={MyComponent} container={document.body} {...otherProps} />

This empties the content of document.body, then mounts MyComponent while passing down otherProps.

Hope that helps.

In the constructor of the component, you could actually clear the contents of the div before rendering your Portal content:

class PrivacyContent extends React.Component {

    constructor(props) {
        super(props);
        const myNode = document.getElementById("privacy");
        while (myNode.firstChild) {
            myNode.removeChild(myNode.firstChild);
        }
    }
    render() {
        return createPortal(
            <div>
                <button onClick={this.handleClick}>
                    Click me
                </button>
            </div>,
            document.getElementById('privacy')
        )
    }

    handleClick() {
        alert('clicked');
    }

}

export default PrivacyContent;

I find this is better and doesn't need useState:

  export const Portal = () => {
  const el = useRef(document.createElement('div'));

  useEffect(() => {
    const current = el.current;
    // We assume `root` exists with '?'
    if (!root?.hasChildNodes()) {
      root?.appendChild(current);
    }

    return () => void root?.removeChild(current);
  }, []);

  return createPortal(<Cmp />, el.current);
};
Related